__asm__ を試してみた

g++でインラインアセンブラを使ってみることにした。g++ -S でアセンブリリストを見ることは多々あったが、思えば__asm__を自分で書いたことはなかったのでした。引数の "=r(ほげほげ..)" の意味など把握しなければならないことがたくさん。

書いてみた


とりあえず書いてみました。以下自分用の備忘録です。


dWの記事や"GCCでインラインアセンブリを使用する方法と留意点等 for x86"などを参考にさせていただいた。あとGCCのマニュアル中の、"C の式をオペランドとするアセンブラ命令"(原文)も。


最初は出力パラメータの"=r"と"=m"の違いがよくわからなかったんだけど、gcc -S をしてみて納得。

int foo;
__asm__("movl $1, %0;" : "=r"(foo));

の場合は、

#APP
movl $1, %eax;
#NO_APP
movl    %eax, -12(%ebp)

となって、__asm__部分(#APP 〜 #NO_APP部分)では一旦レジスタに結果が格納される。#NO_APPの直後で、コンパイラが自動的に自動変数fooのメモリ -12(%ebp) に計算結果を書いているのがわかる。対して、"=m"(foo) の場合は、#APP .. #NO_APP 内でいきなり -12(%ebp) に計算結果が書かれるされるわけですね。


で、通常は"=r"にしとけばよさそうです。最適化が効きやすいはず。次のような加算を行う関数を例にすると、

inline int add(int a, int b) { 
    int ret; 
    __asm__("\ 
      movl %1, %0;\ 
      addl %2, %0;"\ 
      : "=&r"(ret) 
      : "r"(a), "r"(b) ); 
    return ret; 
} 
int main() { 
    int a = add(1, 2); 
    std::printf("%d\n", a); 
} 

これを g++ -O2 -fverbose-asm -S で.sに変換すると、

        movl    $1, %eax  #  a
        movl    $2, %edx  #  b
#APP
              movl %eax, %ecx;    addl %edx, %ecx;      #  a,  b
#NO_APP
        pushl   %ecx    #  ret
        pushl   $.LC0
.LCFI3:
        call    printf

このようになっています。add関数への引数はレジスタ渡しになっているし、計算結果も%ecx上に保持されているものがprintfにpushされているんで、効率的。


これを"=m"(ret) にしてしまうと、

#APP
              movl %edx, -4(%ebp);        addl %eax, -4(%ebp);  #  a,  b
#NO_APP
        pushl   -4(%ebp)

こうなってしまいます。遅い。

リファレンスマニュアル

IA-32 インテルアーキテクチャ・ソフトウェア・デベロッパーズ・マニュアルの日本語版・中巻PDFには命令リファレンスがある。

注意点/pitfalls

__asm__ するときには気をつけねばならない落し穴がいろいろ存在するようですが、まとめてある素晴らしいページがありました。ちょっと引用させてもらいます。

・入力引数に書いてはならない

入力引数レジスタに書き込んだ場合、最適化フーズでどのような処理をされるかわかりません。

・出力引数から読んではいけない

入力の場合と同じです。Read-Modify-Writeを行うときには入力引数の制約を、同じ変数を示す引数番号にします。

・出力引数に書き込んだ後に入力から読んではいけない

コンパイラレジスタを節約するために入力引数と出力引数を同じレジスタに割り当てることがあります。これは非常に重要な注意事項です。どうしても出力のあとに入力引数を読まなければならない場合には出力制約を"=&c"のように&つきで宣言します。

・メモリーへの書き込みに注意

モリーに書き込みを行う場合には、破壊されるレジスタとして"memory"を宣言します。
・最適化を避ける場合にはvolatileを使う
asm volatile (... )とすれば、テンプレート内部に最適化が及ぶことはありません。ただし、やや非効率になります。

或日この話は、上の「入力引数に書いてはいけない」です(よ)ね。

__asm__ か、.s か

アセンブラと戯れるには、__asm__ を使うのではなく、手で .s ファイルを記述する方法もある。しかし、.sで関数作成だとインライン展開されずに必ずcall <func_name> されてしまう。


__asm__でインラインアセンブラなら、Cの変数を%0,%1..で参照できるし、関数の入退場処理(%esp, %ebp の退避復元やらretやら) を自分で書かなくていいしで便利。


なので暫くは __asm__を使うことにしよう。・・まぁ当然か。