Designing for Performanceを和訳してみました。
スマートフォンは従来のケータイに比べて高性能ですが、PCやサーバと比べると限られたリソースしか無いです。
特に重要な違いとして、ノートPCなどと比べてもバッテリーを使用しないことを要求されます。
AndroidはJavaをサポートしているので、スマートフォンの中でも既存の開発知識を使いやすいです。
しかし、一部において、パフォーマンスを改善するために、Javaではよくない とされる手法を使う場合もあり得ます。
この章ではそういう知識が具体例を交えて紹介されていました。
原文
性能のために設計する最高の方法
Androidアプリケーションは限られたパワー、限られた記憶容量、限られたバッテリーを持つモバイルデバイスで実行されます。そのため、効率的に動く必要があります。
たとえ十分に速かったとしても、バッテリーライフはアプリを最適化する理由となります。
バッテリーライフはユーザーに取って重要です。
そしてAndroidはバッテリーを使用して、バッテリーが消費する原因があなたのアプリケーションにあるかをユーザーに知らせます。
ノート
このドキュメントは主にマイクロ最適化についてカバーするが、これがあなたのアプリケーションの成否を握るとは限らない点に注意してください。
正しいアルゴリズムとデータ構造を選択することは常に優先されるべきことですがこのドキュメントの範囲外です。
はじめに
効率的なコードのために2つの基本的なルールがあります。・必要ないことはするな
・可能ならばメモリーを割り当てるな
注意深く最適化する
このドキュメントはAndroid特有のマイクロ最適化について記載しています。そのため、あなたは既にどのコードが最適化されるべきか正確に理解できるプロファイリングを使用し、変更した結果の影響を図る方法があると仮定します。
あなたには費やすべきそれだけの技術的な時間しかないので、懸命にそれを費やしている事を知ることは重要です。
(プロファイリングと効果的なベンチマークについてはおわりにを読んでください)
このドキュメントはあなたがデータ構造とアルゴリズムについて最高の決定し、APIの将来的な問題ついても考慮したと仮定します。
もし、そのようなアドバイスを必要とするのであれば、Josh BlochのEffective Java 項目47を見てください。
Androidアプリをマイクロ最適化することは、アプリが複数のハードウェアプラットフォームで動作す場合慎重を要する問題の一つです。
VMが異なるバージョン、、異なるプロセッサ、異なる速度、あなたは単にデバイスXの要因FはデバイスYより早いまたは遅いということが出来るだけで、1台の装置の結果が他のデバイスで一致するとは限りません。
特にエミュレータの測定は他の殆どの装置の結果と連動しません。
JITを使う場合とそうでない場合もです。
JITのための最適なコードが必ずしもJIT以外で最適なコードではありません。
あなたのアプリがどのデバイスで動くか知っているのであれば、そのデバイスでテストする必要があります。
不要なオブジェクトの生成を避ける
オブジェクトの生成は無償ではありません。一時オブジェクトのためにスレッドの割り当てをプールする世代のガベージコレクションはより低負荷ですが。しかし、メモリーを割り当てることはメモリーを割り当てないより常に高負荷です。
Gingerbreadの並列コレクターはこれを助けます。しかし、不要な仕事は常に避けるべきです。
このように、不要なインスタンスの生成は避けるべきです。
この事例のいくつかのヘルプ:
- もし、Stringを返すメソッドを持っており、その戻り値を常にStringBufferにより追加される事を知っているのであれば、一時的なオブジェクトを作る代わりに直接appendを呼ぶようにシグネチャと実装を変えてください。
- 入力データからStringを得るときはコピーを作成する代わりに最初のデータのSubstringを返そうとしてください。
新しいString オブジェクトを作りますがそれはchar[]とデータを共有します。
やや過激なアイデアは多次元配列を複数の1次元配列にわけます。
Integerの配列よりintの配列がいいです。これは(int,int)の多次元配列より2つのint配列のほうがより効率的であるという一般論を意味します。
その他プリミティブな型においても同様です。
もし、(Foo,Bar)オブジェクトの配列を実装しようとしているならば、2つのFoo[] Bar[]配列が普通はより効率的になることを思い出してください。
一般的に、可能なかぎり短期的な一時オブジェクトを作ることを避けてください。
作られるオブジェクトの数が減れば、ガベージコレクションの頻度は少なくなり、それはユーザーの操作性に直接的なインパクトを与えます。
パフォーマンス神話
以前のバージョンでこのドキュメントでは紛らわしい事を書いていました。ここでそれを整理します。
JITが無いデバイスではインターフェイスより具体的なオブジェクト型の変数からメソッドを呼び出したほうがわずかに効率的なのは事実です。
例えば両方のケースでMapがHashMapだとするとHashMap型の変数でメソッドを呼び出すほうがMap型の変数より速いです。
これは2倍遅くなるといった話ではありません。
実際のところは6%程度の違いです。しかも、JITはそれら二つの差を効果的になくします。
JITの無いデバイスでは、キャッシュフィールドによるアクセスは繰り返しフィールドにアクセスするより約20%速くなります。
JITでは、フィールドにアクセスするコストはローカルにアクセスするコストとほぼ同じです。
そのためコードが読みやすくなるのでないならば、価値がある最適化とは言えません。
これはfinal,staticそしてstatic finalフィールドに対しても当てはまります。
Staticを好む
もしオブジェクトフィールドにアクセスする必要が無いのであればメソッドをstaticにしてください。そのおまじないにより15〜20%はやくなります。
それはいい方法でもあります。
なぜなら、メソッドがオブジェクトの状態を変えることがないとメソッドのシグネチャで宣言できるからです。
内部のgetter,setterを避ける。
C++のようなネイティブ言語では、(i=mCountのように)直接フィールドにアクセスする代わりに(i = getCount()のような)getterを使うことはよくあります。これはC++のための優れた習慣です。
なぜなら、コンパイラが直接インラインでアクセスすることが出来て、あなたが必要となれば、いつでもアクセスを制限したりデバッグしたりしたコードを加えることができるからです。
これはAndoridではいいアイデアと言えません。
JITが無いとき、直接フィールドにアクセスするのはgetterより約3倍速いです。
JITがあると、(直接フィールドにアクセスするのがローカルアクセス並みに早くなったので)直接フィールドにアクセスするのはgetterより約7倍速いです。
これはFroyoについて真実です。しかし、将来JITがインラインでgetterメソッドを実行するとき改善されます。
定数のためにStatic Finalを使用する。
クラスの上位にある以下の宣言について考えてみます。
static int intVal = 42;
static String strVal = "Hello, world!";
コンパイラはclinitをコールする初期化メソッドを作成し、クラスが最初に使用されるときに呼び出されます。
メソッドはintValに42を登録し、クラスファイルのStringコンスタントテーブルよりstrValのための参照を引き出します。
finalでこの面倒を改善することが出来ます。
static final int intVal = 42;
static final String strVal = "Hello, world!";
クラスはclinitをもはや必要としません。
intValは直接42を使用し、strValへのアクセスはフィールド参照より安価な文字列定数を使用します。
ノート
この最適化は任意の参照型には当てはまらずプライマリタイプとString定数にだけ当てはまる事に注意してください。
しかし、可能なかぎり定数をstatic finalとするのは良い手法です。
強化されたfor loop文法を使用する。
強化されたループ(for eachとも言われる)はiterableなインターフェイスを実装するコレクションと配列のために使用できます。iteratorインターフェイスによってコレクションにhasNext()とnext()が割り当てられます。
手書きでカウントされるArrayListのループは約3倍速いです。(JITの有無に関わらず)
しかし他のコレクションに対してのループ構文は正確にiteratorを使用した場合と等しいです。
配列を繰り返す案がいくつかあります。
static class Foo {
int mSplat;
}
Foo[] mArray = ...
public void zero() {
int sum = 0;
for (int i = 0; i < mArray.length; ++i) {
sum += mArray[i].mSplat;
}
}
public void one() {
int sum = 0;
Foo[] localArray = mArray;
int len = localArray.length;
for (int i = 0; i < len; ++i) {
sum += localArray[i].mSplat;
}
}
public void two() {
int sum = 0;
for (Foo a : mArray) {
sum += a.mSplat;
}
}
zero()は最も遅いです。
なぜなら、JITが配列長を取得するためのコストを最適化することがまだ出来ないためです。
one()はそれより速いです。
ローカル変数を参照しルックアップを避けます。
array lengthだけはパフォーマンスを提供します。
two()はJITの無い端末では最速で、JITがある端末ではone()と同等です。
Java1.5で採用された強化されるループ構文を使用します。
以下をまとめます。
通常強化されたループを使用してください。
しかし、パフォーマンスが重要なArrayListの繰り返しのために手書きで数えられるループを考慮してください。
Effective Java項目46 を見てください。
PrivateインナークラスとPrivateアクセスの代わりにパッケージを考慮してください
以下のクラス定義を考えてみてください。
public class Foo {
private class Inner {
void stuff() {
Foo.this.doStuff(Foo.this.mValue);
}
}
private int mValue;
public void run() {
Inner in = new Inner();
mValue = 27;
in.stuff();
}
private void doStuff(int value) {
System.out.println("Value is " + value);
}
}
ここで注意すべきのは、private inner classとして定めた内部クラス(Foo$Inner)が外のプライベートメソッドとインスタンスフィールドに直接アクセスしているということです。
これは正しくコードは「Value is 27」を出力します。
問題はJava言語として適切であったとしても、VMがFooとFoo$Innerが別のクラスなので、直接のアクセスは許可されていないと考える事です。
このギャップを埋めるために、コンパイラは2つのメソッドを生み出します。
/*package*/ static int Foo.access$100(Foo foo) {
return foo.mValue;
}
/*package*/ static void Foo.access$200(Foo foo, int value) {
foo.doStuff(value);
}
インナークラスがアウタークラスのmValueフィールドかdoStuffメソッドを呼び出す必要があるときはこれらのstaticメソッドを使用します。
これが意味することはメンバーフィールドにアクセッサーメソッドでアクセスするコードが現実になるということです。
既に直接的なアクセスよりアクセッサがどれだけ遅いか説明しているように、これはパフォーマンスに影響する言語特有の見えないイデオムの例です。
もし、高性能を必要とする地点で使用するなら、内部クラスから呼ばれるフィールドとメソッドをプライベートからパッケージに変更することでオーバーヘッドを回避できます。
残念ながら、この方法は同じパッケージの別のクラスから直接フィールドを操作できることを意味するため、広く公開するAPIでは使用すべきではありません。
浮動小数点を注意深く使う。
経験上、浮動小数点はAndroidのデバイスで2倍程度遅いです。これはFPUとJITがないG1とFPUとJITがあるNexus Oneで事実です。
もちろん、2台の端末には10倍の絶対的な速度差があります。
最新のハードウェアにおいてdoubleとfloatにこの性能差はありません。
サイズはdoubleが2倍広く、デスクトップと同様にサイズが問題でなければ、floatよりdoubleを好む必要があります。
また、Integerでさえ、いくつかのチップでは複数のハードウェアを持つが、ハードウェアでの分割機能を欠如しています。
そのような場合、整数の分割と係数はソフトウェアで処理されます。
いくつか考えるべきは、ハッシュテーブルを設計している時や、沢山の数学を行う必要がある時です。
ライブラリを知り、使う
あなたの処理を実行するよりライブラリコードを好む理由があります。通常の理由に加えて、システムは自由に手作業で作られたアセンブラコード(JITがJavaで作成できる最高のコードより良い場合がある)を呼べることを覚えておいてください。
典型的な例はString.indexOfとその仲間です。これはDlavicで代わりにインラインされています。
同様にSystem.arraycopyメソッドは手作業で作るコードよりNexus oneにおいて9倍速いです。
Effective java 項目47を読んでください。
ネイティブコードを注意深く使う
ネイティブコードが必ずしもJavaより効率的であるとは限りません。一つはJavaとネイティブコードを移行するコストがあります。加えて、JITはそれらをまたがって最適化出来ません。
もし、ネイティブリソース(ネイティブヒープ上のメモリ、ファイル記載子など)を割り当てると、これらのリソースのタイムリーなコレクションを取得するのはかなり難しくなります。
あなた自身もまたアーキテクチャごとにコンパイルを行う必要があります。
同じアーキテクチャに見えるものに対してさえ複数のバージョンに対してコンパイルする必要があるかもしれません。
G1のARMプロセッサ向けにコンパイルされたネイティブコードはNexus OneのARMプロセッサで最大の性能を活かすことが出来ません。そしてNexus OneのARMプロセッサ向けにコンパイルされたネイティブコードはG1のARMプロセッサで動きません。
ネイティブコードはJavaアプリの速度をあげることより、Android向けの既存コードベースを持つときに主に役立ちます。
ネイティブコードを使う必要があるなら、JNIチップスを読む必要があります。
(Effective Java項目54を読んでください)
おわりに
最後に、常に測定してください。最適化を行う前に問題があることを確認して下さい。
現在のパフォーマンスを正しく測定出来ることを確認してください。
さもなければ、試した結果を測ることが出来ません。
このドキュメントによるあらゆる主張はベンチマークによって裏付けされます。
ベンチマークのためのソースはcode.google.com "dalvik" project.に有ります。
ベンチマークはJavaのマイクロベンチマークによってビルドされています。
マイクロベンチマークは正しく測るのが難しいので、カリパスはあなたのために困難な作業をし、あなたが測っていないケースさえ見つける努力をします(なぜならVMあなたのコードを最適化することが出来るので)
マイクロベンチマークを走らせるためにカリパスを使うことを我々は強く推奨します。
Traceviewも役立つと思うかもしれません。
しかし、それが今のところJITを無効化してしまうことを知っておく必要があります。
そしてJITが戻ってくるときにコード特有の問題を起こすことがあります。
それはTraceviewデータで動かしたあとにTraceview無しで動くとき、結果として生じるコードが実際に早くなるようにTraceviewデータを使うときに特に重要です。
Except as noted, this content is licensed under Apache 2.0.