ARMプロセッサを使ったプログラムを最適化する
この記事は mbed Advent Calendar 2015 - Adventar の20日目の記事です。
ARMプロセッサを使ったプログラムを最適化する
mbed というか、ARMプロセッサを使ったプログラミングの話を書きます。
プログラムの最適化については、通常はコンパイラが様々な最適化を行ってくれますが、言語仕様や ABI (Application Binary Interface) の規定によって、コンパイラやツールなどでは勝手に変更を加えることが出来ない約束事があります。開発者がこの規定をある程度事前に理解していると、コーディングレベルで効率の良いプログラミングを行うことが出来ます。
変数の型を考える
ARMプロセッサコアでC/C++言語を使ってプログラミングする場合、変数の型は取り扱う値の上限が決まっているのであれば、その上限値が取り扱える最小のサイズを使おうとする事が多いと思います。
サイズの大きな配列変数や、複数のインスタンスが生成される可能性のある構造体やクラスの要素は、そのように設計するのが良いと思います。
では、関数内で一時的に使用される作業用の変数の型は何を使用するのが効率的だと思いますか?
例えば、以下のようなコードを記述したとします。
int foo(int a) { unsigned char count; for(count = 0; count < 10; count++) { a++; } return a; }
ここでは、カウント用の変数 count をunsigned char にしていますが、これをコンパイルすると以下のようなコードが生成されます。使用したコンパイラは ARM Compiler 5.04 update 2 (build 82) 、コンパイルオプションは、armcc -c --cpu=7-m test.c --asm
です。
||foo|| PROC MOVS r1,#0 |L1.2| ADDS r1,r1,#1 UXTB r1,r1 ADDS r0,r0,#1 CMP r1,#0xa BCC |L1.2| BX lr ENDP
r1 のインクリメント処理を行った後に、UXTB 命令が使用されています。これはバイトサイズのゼロ拡張命令で、8 ビット値を 32 ビット値に拡張するのに使用されます。 http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0489ij/CIHCAJAB.html
for ループ内の変数 count は、ワードサイズ(ARMアーキテクチャでは、32ビット)で比較を行う必要があるので、この UXTB 命令が必要になります。
このような場合 32-bit のARMプロセッサでは、unsigned long, unsigned int, uint32_t 型(32ビット符号無し変数)を使用するのが効率的です。上記のコードを以下のように書き直して count 変数の型を unsigned char から unsigned int に変更します。
int foo(int a) { unsigned int count; for(count = 0; count < 10; count++) { a++; } return a; }
これによって、ゼロ拡張されないコードが生成されました。
||foo|| PROC MOVS r1,#0 |L1.2| ADDS r0,r0,#1 ADDS r1,r1,#1 CMP r1,#0xa BCC |L1.2| BX lr ENDP
このように、汎用レジスタでの比較を行う処理では、変数の型を最適化することによって生成されるコードを少なくすることが出来ます。
forループの最適化
さて先ほどの関数内での for ループですが、アップカウントからダウンカウントに変更し、終了判定を 0 にするともう少しだけコードを小さく出来ます。
int foo(int a) { unsigned int count; for(count = 9; count != 0; count--) { a++; } return a; }
終了判定が 0 の場合は、演算結果の Z フラグを参照する条件分岐命令 (BNE: Branch Not Equal) を使うことが出来ます。このコードの場合は、比較命令のコード分少なくなります。
||foo|| PROC MOVS r1,#9 |L1.2| ADDS r0,r0,#1 SUBS r1,r1,#1 BNE |L1.2| BX lr ENDP
関数のパラメータの最適化
C/C++ 言語で関数呼び出しを行う場合の引数の使い方は、AAPCS (Procedure Call Standard for ARM Architecture) で規定されています。 http://infocenter.arm.com/help/topic/com.arm.doc.ihi0042e/index.html
このドキュメントには、データのアラインメントや引数に割り当てられるレジスタ等の情報が記載されています。 引数の使い方を工夫することによって、効率よく関数呼び出しを行うことが出来ます。
基本的なルール
- 引数は順番に、R0からR3に割り当てられ、一つの引数に対して一つ以上のレジスタが使われる
void foo(int a, int b, int c , int d); /* レジスタ : 変数 R0 : a R1 : b R2 : c R3 : d */
- 5番目以降の引数はスタック経由で渡される
void foo(int a, int b, int c , int d, int e); /* レジスタ : 変数 R0 : a R1 : b R2 : c R3 : d e : スタック渡し(callerでスタックにストアされ、calleeでスタックから汎用レジスタにロードされる) */
- C++ のメンバ関数の場合は、R0はthisポインタが渡される
void foo:bar(int a, int b, int c, int d); /* レジスタ : 変数 R0 : thisポインタ R1 : a R2 : b R3 : c d : スタック渡し */
- R0からR3には、32ビット以下のサイズの変数が渡される
void foo(char a, short b, int c, long d); /* レジスタ : 変数 R0 : a R1 : b R2 : c R3 : d */
- 32ビットを越える変数は、R0-R1またはR2-R3の組み合わせ(偶数+それに続く奇数レジスタ)で値が渡される
void foo(double a, int b, int c); /* レジスタ : 変数 R0-1 : a R2 : b R3 : c */
void foo(int a, double b, int c); /* レジスタ : 変数 R0 : a R1 : 使用されない R2-3 : b c : スタック渡し */
関数の引数の順番を工夫することで、効率的に値を渡すことが出来ます。
- 引数の数は必要以上に多くしない(4個以上になる場合は、使用頻度の高い引数を先頭に配置する→レジスタに載るので呼び出し先でロード処理が必要ない)
- double, long long, int64_t 等の32-bitを越える変数はなるべく使用しない(使用する場合は、偶数番目のレジスタに乗るように一番目か三番目の引数にする)
最後に
「そんな最適化はコンパイラで勝手にやって欲しい」と思われる方もいるかもしれません。
もちろん、コードに影響をしない最適化は積極的にコンパイラで行いますが、明らかに意図されないコード生成はコンパイラとして許されません。特に異なるコンパイル単位のオブジェクトは確実にリンクされる必要があるので、コンパイラは ABI を正しく守る必要があります。これによって、異なるベンダーのツールチェインでコンパイルされたスタティックライブラリ等を利用することが出来ます。
今回説明した規定や特徴を理解してコーディング時に少し工夫することによって、コードサイズを小さくしたり、実行速度を速くすることが出来ます。
こちらの資料もご覧下さい。
https://developer.mbed.org/users/MACRUM/notebook/arm-cortex-m-mbed-sdk-and-hdk-deep-dive-20140704/
以上です。
明日は ytsuboi さんです。よろしくお願いします。
Please log in to post comments.