インライン・アセンブラは、高速化のためだけではなく命令実行サイクルによる時間計測などC/C++言語では不可能な問題を解決するために必要である。最大のメリットはC/C++言語と一体でコンパイルされることに尽きるがC/C++の言語仕様のメリットをアセンブラでも活用したいと考えるならインライン・アセンブラ一しかない。
まず、C/C++関数からインライン・アセンブラ関数をコールするさいの引数や戻り値の返し方及びレジスタの使い方に関するルールは最低限知っておく必要がある。それさえわかれば後は命令表を見ながらでも使い始めることはできるはずだ。
但し、コンパイラ仕様に絡んで注意しなければならないことがいくつかある。特に注意が必要なのは最適化によりインライン・アセンブラ関数がコール元のC/C++コードの内にインライン展開されてしまう場合だ。インライン展開された場合は引数の渡し方や戻り値のルールは適用されないとか、それ以外にもいくつか知っておいたほうが良いことがあるので順に説明したいと思う。
【インライン展開(Inline Expansion)】
例えば、AVRでは第一引数がr24レジスタに設定されるがインライン展開されたときも同様にr24に第一引数が設定されるとは限らないのでインライン・アセンブラの入出力パラメタにて引数を参照するのが正しい方法である。そうしない場合はインライン展開させないようにnoinline属性を指定しなければいけない。
逆にインライン展開させたい場合はalways_inline属性を指定するが常にインライン展開できるとは限らないことに注意。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/* 正式な記述方法の例 */ uint8_t add(uint8_t a, uint8_t b) { uint8_t ret; asm volatile ("mov %0, %1" : "=r" (ret): "r" (a)); asm volatile ("add %0, %1" : "=r" (ret): "r" (b)); return ret; } /* インライン展開できないコード例 */ uint8_t add(uint8_t a, uint8_t b) __attribute__((noinline, naked)); uint8_t add(uint8_t a, uint8_t b) { asm volatile ("add r24, r22"); asm volatile ("ret"); } |
【プロローグとエピローグ(Prologue Epilogue)】
戻り値を持つ関数では戻り値を格納するための変数宣言が必要であるがインライン展開されないという前提ならレジスタに直接設定したほうが簡潔に記述できたりする。しかし、コンパイラが生成するプロローグやエピローグ・コードがそうすることを邪魔してしまうためnoinlineに加えnaked属性も指定する必要がある。naked属性によりリターン命令も生成されなくなるためリターン命令を追加する必要があることにも注意。
1 2 3 4 5 6 |
uint8_t func() __attribute__((noinline, naked)); uint8_t func() { asm volatile ("mov r24, 0"); /* 戻り値 */ asm volatile ("ret"); } |
【クロバーリスト(Clobber List)】
変数割り当てせずに利用するレジスタはインライン・アセンブラのクロバーリストと呼ばれるパラメタに指定しておくとコンパイラがレジスタ割り当てを調整してくれるので忘れずに指定するべきだ。C/C++コードとインライン・アセンブラを関数内で混在させる場合には必ず指定しなければならない。C/C++コードからインライン・アセンブラを利用する場合には様々な調整をしてくれるが逆にインライン・アセンブラからC/C++コードを利用する場合には何も考慮してくれないことに注意が必要となる。
1 |
asm volatile ("mov r24, r25" ::: "r24"); |
【最適化(Optimize)】
最適化レベルによってはインライン・アセンブラがコンパイルできなくなる場合があるためインライン・アセンブラで記述する場合は最適化レベルを属性指定したほうが良い。
1 |
void func() __attribute__((optimize(1))); |
【デッドコード削除(Dead Code Elimination)】
C/C++言語の最適化の一部であるデッドコード削除を利用してテンプレート引数や関数引数でコードを切り替えることができる。次の関数は同じコードが生成されるがfunc2()については最適化が有効になっていること、引数に定数が指定されていること、および、インライン展開されることが前提となる。テンプレートが一番柔軟性がありお勧めだ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
void func0() { #if F_CPU < 16000000 asm volatile ("..."); #else asm volatile ("..."); #endif } template<long F_CPU> void func1() { if (F_CPU < 16000000) asm volatile ("..."); else asm volatile ("..."); } void func2(long F_CPU) __attribute__((optimize(1))); // 最適化を有効にしておく void func2(long F_CPU) // F_CPUには定数を指定する { if (F_CPU < 16000000) asm volatile ("..."); else asm volatile ("..."); } |
【逆アセンブル(Disassemble)】
アセンブラでは記述した通りにコンパイルされるが、インライン・アセンブラはコンパイラの最適化により命令が追加されたり変更されたりすることがある。意図した通りのコードになっているか生成されたバイナリをobjdumpコマンドにより逆アセンブルし確認すべきである。
【最後に】
まだなんかあったような気もするがド忘れしてしまった...思い出したら追記していくことにしよう...(-_-;)
以上、ドキュメントにも記載がなく経験で知りえたことではあるが覚えておいても損はないと思う。