ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。

Android アプリ開発独習メモ #5 — パフォーマンスに関するチップス

< Android アプリ開発独習メモ #4 | Android アプリ開発独習メモ #6 >

Google による Android アプリ開発のガイドを読み進めていくシリーズ。 今回は “Best Practices for Performance” の中の “Performance Tips” を読んだ。 ざっくりとまとめておく。

パフォーマンスに関して神経質になる必要はないと思うけど、それなりに気を付けないとスクロール時にガタガタしたりしてしまったりするので、一通り知識としては頭に入れておきたいところだと感じた。

Performance Tips

ミクロなレベルでの最適化の話。 当然ながら正しいアルゴリズムやデータ構造を選択するのは大事! (今回はそういう話ではない。)

効率のよいコードを書くための基本的な規則は次の 2 つ:

  • 必要ない処理をしない。
  • 避けられるならメモリ割り当てを行わない。

様々な種類のデバイスで実行することを考えると、VM のバージョンの違いによって、あるデバイスでは効率の良いコードであっても別のデバイスではそうでないこともある。 JIT の有無によっても効率の良いコードが何かは違ってくる。 エミュレータでの計測では、パフォーマンスについて、本当に少しのことしかわからない。

Avoid Creating Unnecessary Objects

オブジェクトの生成はできるだけ避ける、ということ。 例えば以下のような例。

  • 文字列を返すメソッドがあったとして、そのメソッドの返り値が常に StringBuffer オブジェクトに追加されるのであれば、そもそものメソッドのシグニチャを変更して、引数として StringBuffer オブジェクトを受け取って、メソッドの中で追加するようにすればよい。 (そうすれば返り値としての String オブジェクトの生成が避けられる。)
  • Integer 型の配列よりも、int 型の配列の方がよい。
  • 2 次元配列 (配列の配列) を作るよりも、1 次元配列で代替できるならばその方が良い。
  • Foo オブジェクトと Bar オブジェクトの組を配列として保持したい場合、Foo オブジェクトと Bar オブジェクトの組を表す独自のクラスを作り、そのクラスの配列を生成することも考えられるが、それよりも Foo の配列と Bar の配列をそれぞれ別に作って管理した方が良い。

一般化すると、短期的に使用される一時的なオブジェクトの生成は避けるべし、ということ。 オブジェクトの生成が少ないと、それだけガベージコレクションの頻度が小さくなるので、ユーザー体験に影響する。

基本的に保守しやすさとパフォーマンスのトレードオフだと思うけど、PC と比べてリソースが少ないわけなので、PC 向けにプログラミングするときよりもリソースの消費には気を使わなければならない、というところか。

Prefer Static Over Virtual

オブジェクトのフィールドへのアクセスが必要ないメソッドならば、インスタンスメソッドではなく static メソッドにすべき、という話。 呼び出しが 15 % から 20 % ほど速くなるらしい。 メソッド呼び出しのコストが気になる場所ってそんなあるのかどうかわかんないけど。

『Prefer Static Over Virtual』 って見出しになってるけど、これってつまり virtual かどうかが重要なのであって、static かどうかが重要ってわけでもないのかな。 (つまり、final メソッドなら static じゃなくても速いのだろうか。)

Use Static Final For Constants

定数変数は static かつ final にしておきましょう、って話。

そうすることで、変数が使っている箇所に値が直接埋め込まれるので、クラス初期化用の <clinit> ってメソッドが必要なくなる *1 し、変数のルックアップのコストもなくなる。 プリミティブ型と String 型にしかこの最適化は適用されないけど、他の型についても、定数は static かつ final にしておいた方がいい。

Avoid Internal Getters/Setters

C++C#Java のベストプラクティスとして、直接的なフィールドアクセスよりも setter/getter を使った方が良い、というものがあるが、Android ではそれはしない方が良い。 virtual メソッドの呼び出しはインスタンスフィールドのルックアップよりもコストが大きい。

公開インターフェイスとして setter/getter を用意するのは良いが、例えばクラス内で使用する場合は直接フィールドアクセスするべきとのこと。 JIT 無しの場合は単なる getter よりも直接的なフィールドアクセスの方が 3 倍速く、JIT 有りの場合は 7 倍速いらしい。

ProGuard を使う場合は、それがアクセサのインライン化をしてくれるので、setter/getter を使っても直接的なフィールドアクセスでも良い、みたい。

Use Enhanced For Loop Syntax

for-each ループを使った方が、手書きによるカウントアップの for ループより速い場合が多いので、基本的に for-each ループを使いましょう。

    int sum = 0;
    for (Foo a : mArray) {
        sum += a.mSplat;
    }

ただし、ArrayList に関しては手書きによるカウントアップの for ループの方が for-each ループの 3 倍速いらしい。

Consider Package Instead of Private Access with Private Inner Classes

内部クラスからエンクロージングクラスの private メソッドを呼び出したりや private フィールドにアクセスしたりするのは、パッケージプライベートなメソッドを呼び出したり、パッケージプライベートなフィールドにアクセスしたりするよりもコストがかかる。 *2

なので、パフォーマンス的に重要な場所で内部クラスからエンクロージングクラスの private メンバにアクセスしている場合は、その private メンバをパッケージプライベートに変更すると良い。

Avoid Using Floating-Point

浮動小数点数より整数値の方が速い! (ので、整数値でいいところは整数値を使おう。)

Know and Use the Libraries

JIT 用に最適化されてたりするので、ライブラリメソッドがあるならそれを使おう。

Use Native Methods Carefully

Android NDK を使ってネイティブコードを書くのは、主には既存のコードを Android アプリ用に移植するためのものであって、Java で書かれたアプリの高速化のためのものではない、とのこと。

Performance Myths

JIT 無しのデバイスでは、実際の型の変数を通してのメソッド呼び出しの方が、インターフェイス経由のそれよりもわずかに効率が良い。 JIT 有りの場合は区別できない。

JIT 無しのデバイスの場合、フィールドの値を局所変数にキャッシュするとフィールドアクセスを繰り返すよりも約 20 % 高速である。 JIT 有りの場合は局所変数へのアクセスもフィールドへのアクセスもコストは変わらない。 なので、変更によりコードが読みやすくなると考えられないならば、やる価値はないだろう。

Always Measure

最適化するときには、解決すべき問題があることを認識しておかなければならない。 (やみくもにパフォーマンスを上げようとするのではなく、遅い部分があるからこそ最適化しようとするものだ、ってことだと思う。) 変更前のパフォーマンスを計測することができて、変更して比較できるように。

この資料で述べられていることは、全てベンチマークによる裏付けがある。 それらのベンチマークcode.google.com "dalvik" project にある。

これらのベンチマークCaliper という Java 用のマイクロベンチマークのためのフレームワークを使って行われた。 マイクロベンチマークを正しく行うのは難しく、Caliper は役立つものであるとのこと。 マイクロベンチマークには Caliper を使うのがオススメらしい。

Traceview というのもプロファイリングに便利である。 ただし、現在のところこれは JIT を無効化することを認識しておく必要がある。

プロファイリングやデバッギングに役立つ文書:

まとめ

Effective Java 第2版 (The Java Series)

Effective Java 第2版 (The Java Series)

細かい話が多いが、普段開発をするうえではそこまで神経質にならなくてもいいのではないかなーという感じがした。 (例えば内部クラスからエンクロージングクラスの private メンバにアクセスする話とか。)

しかし、例えば描画処理をするごとにメモリ割り当てをしてしまうと、ガベージコレクションが頻発してスクロール時にガタガタしてしまう、というようなことがあるので、パフォーマンスのチップスにはちゃんと身に付けて、避けるべき部分はしっかり避けていかなければならないと思う。

< Android アプリ開発独習メモ #4 | Android アプリ開発独習メモ #6 >

*1:定数変数じゃないものが他にあればどっちにしろ必要だと思うけど。

*2:内部クラスからエンクロージングクラスの private なメンバにアクセスできるという言語仕様ではあるが、実際のところ別クラスの private なメンバにアクセスするわけなので、そのギャップを埋めるためにコンパイラが仲介用のメソッドを生成するようになっている。 仲介用のメソッドを経由する必要があるので、コストがかかる。