チューニング 番外編「ARM向けの最適化」
ここまではヒープ領域の管理方法に着目してパフォーマンスチューニングを行ってきましたが、少し趣旨を変えて、ここではARM向けの最適化手法を紹介します。Android 5.0の登場と時を同じくして、NDKでも64ビットのARMがサポートされましたが、本章では現在最も普及しているARM v7a(32ビット)の環境を対象としています。
省メモリとアライメント
スマホアプリでは利用可能なメモリは限られているので、極力無駄なメモリを利用しないようにするべきですが、省メモリはCPU側の負担になる場合があります。
プロセッサの種類にもよりますが、一般にワード(4バイト)の処理が最も高速でハーフワード(2バイト)やバイト(1バイト)の処理には「ペナルティ」が付き、数サイクルの遅延が出ます。例えば、省メモリを目的として int 型(4バイト)を char 型(1バイト)に変更して3バイト節約したつもりが、CPU側に負担を強いる結果になります。省メモリが必要なのか、それとも計算速度が必要なのか、優先順位をしっかり決めて設計するべきです。
また、ARMプロセッサはメモリの「アライメント」が正しいことを大前提として動きます。アライメントとは、簡単に説明すると4バイトの変数を定義した場合、4バイト区切り(4の倍数)のアドレスに配置する必要があるということです。x86プロセッサではアライメントがずれている場合でも通常1サイクルのところを2サイクルに分けて読み込みが行われて正常に動作しますが、ARMの場合はそもそも正常に動作しなくなります。
アライメントは、基本的にはコンパイラが自動的に合わせてくれるため、普段は意識する必要はありません。しかし、その結果として開発者が省メモリしたつもりでも全く省メモリになっていない場合があります。
次のC言語のコードを見てください。int 型=4バイト、char 型=1バイトとします。
struct struct_a {
int int_field_1;
char char_field_2;
int int_field_3;
}
この struct_a 構造体は、int 型データの間に char 型データが挟まっているので、そのまま配置してしまうと int_field_3 のアライメントがずれてしまいます。そこでコンパイラは、3バイトのパディング(詰め物)を char_field_2 と int_field_3 の間に入れてアライメントを合わせます、このため、この構造体のサイズは12バイトとなります。
では、次の場合はどうなるでしょう?
struct struct_b {
int int_field_1;
int int_field_3;
char char_field_2;
}
実は、このように配置しても sizeof で返される数値は12となります。なぜかというと、sizeof は、malloc(sizeof(struct struct_b) * length) のように連続領域や配列を確保してもアライメントが合うサイズを返すからです。
配列で確保しない場合は、malloc(sizeof(struct struct_b) - 3) のようにすれば無駄はなくなりますが、 空いた領域は4バイト境界ではないので有効活用が難しく、結局空いたままになる可能性が高くなります。
結果として、この場合は char_field_2 を int 型にしてしまったほうが効率が良いといえます。char 型の場合、前述のとおりメモリからレジスタに読み込む LDR 命令でペナルティが発生しますが、int 型ならペナルティが発生しないからです。
除算(割り算)・剰余(余り)をなるべく利用しない
実は、ARM v7aには除算・剰余命令がありません。除算・剰余はシフト演算・減算をループで繰り返すマクロで代用されますが、何サイクルにも及ぶ演算が必要で、効率は非常に悪くなります。何度も演算が行われる部分ではなるべく除算や剰余を利用しないようにすると効果的です。
スライドパズルアプリで除算や乗算を排除した例を1つ挙げます。
盤面は、図3のように各セルに左上から右下に向けて0~の数値を振って位置を識別しています。盤面の外や壁には移動できないので、移動可能な方向には制限があります。
position を移動の起点、width を盤面の幅とすると、左に移動可能かどうかを調べるには position % width > 0 が true となる必要があります。しかし、この計算には剰余の計算が必要です。
そこで、初期化時にすべての計算を終わらせておくという方法をとりました。図4のような canMoveMap を盤面の初期化時に作成しておき、移動可能かどうかを canMoveMap[position] & 1 << direction) != 0 という判定で行えるようにしています。direction は、LEFT=0,RIGHT=1,UP=2,DOWN=3 です。
移動可否の判定は最も多く行われるオペレーションの1つであるために削減効果は高く、NDK版で1割程度、実行速度が改善しました。このテクニックはNDKだけでなく、Dalvik/ARTでも意識しておくべきものです。
最後に
スマホアプリの開発とチューニングをしていると、時代に逆行しているような感覚にとらわれることがあります。制限事項が多い環境ほど、エンジニアとして基礎力を求められるシチュエーションが多くなります。本稿が、Androidアプリのパフォーマンス不足に悩んでいる方の一助になれば幸いです。
