web-dev-qa-db-ja.com

「asm」、「__ asm」、「__ asm__」の違いは何ですか?

私が知る限り、___asm { ... };_と__asm__("...");の唯一の違いは、最初のものが_mov eax, var_を使用し、2番目が_movl %0, %%eax_を:"=r" (var)とともに使用することです。終わり。他にどんな違いがありますか?そして、asmはどうですか?

35
Adrian

どちらを使用するかは、コンパイラによって異なります。これはC言語のような標準ではありません。

16
Ben Voigt

MSVCインラインasmとGNU Cインラインasmの間には大きな違いがあります。GCC構文は、単一の命令などをラップするために、無駄な命令なしで最適な出力が得られるように設計されています。MSVC構文はかなりシンプルになるように設計されています、しかし、AFAICTは、入力と出力のために、メモリを介した往復の待ち時間と追加の指示なしに使用することは不可能です。

パフォーマンス上の理由からインラインasmを使用している場合、これはMSVCインラインasmを実行可能にします。これは、ループ全体をasmで記述した場合にのみ実行可能です。インライン関数で短いシーケンスをラップするためではありません。以下の例(idivを関数でラップする)は、MSVCが得意としない種類のものです:〜8の追加のストア/ロード命令。

MSVCインラインasm(MSVCおよびおそらくiccで使用され、一部の商用コンパイラでも使用可能):

  • asmを調べて、コードステップを登録するレジスタを特定します。
  • メモリ経由でのみデータを転送できます。レジスターに存在していたデータは、例えば_mov ecx, shift_count_の準備のためにコンパイラーによって保管されます。そのため、コンパイラーが生成しない単一のasm命令を使用するには、出入りの途中でメモリーを往復する必要があります。
  • 初心者にやさしいですが、データを入出力するためのオーバーヘッドを回避することはしばしば不可能です。構文の制限に加えて、MSVCの現在のバージョンのオプティマイザーは、インラインasmブロックの周りの最適化にも適していません。

GNU Cインラインasm は、asm を学ぶ良い方法ではありません。コードについてコンパイラーに伝えるために、asmを十分に理解する必要があります。また、コンパイラが知っておくべきことを理解する必要があります。その回答には、他のインラインasmガイドとQ&Aへのリンクもあります。 x86 タグwikiには、一般的にasmに役立つものがたくさんありますが、GNU inline asmにリンクしているだけです。その答えはGNUインラインasmにも適用されます。)

GNU Cインラインasm構文は、gcc、clang、icc、およびGNU C:

  • コンパイラーに何を破壊するかを指示する必要があります。これを実行しないと、周囲のコードがわかりにくいデバッグ方法で破損します。
  • 強力であるが読みにくく、読みにくく、構文を使用して、入力を提供する方法と出力を見つける場所をコンパイラに伝える。例えば"c" (shift_count)は、インラインasmが実行される前に、コンパイラに_shift_count_変数をecxに入れさせます。
  • asmは文字列定数の内部にある必要があるため、コードの大きなブロックでは余分に扱いにくいです。したがって、通常は

    _"insn   %[inputvar], %%reg\n\t"       // comment
    "insn2  %%reg, %[outputvar]\n\t"
    _
  • 非常に寛容/より困難ですが、特にオーバーヘッドを低くすることができます。単一の命令をラップするため。 (単一の命令をラップすることが元々の設計意図でした。そのため、問題が発生した場合に、入力と出力に同じレジスタを使用しないように、初期のクローバーについてコンパイラに特別に通知する必要があります。)


例:全角整数除算(div

32ビットCPUでは、64ビット整数を32ビット整数で除算するか、完全乗算(32x32-> 64)を行うと、インラインasmのメリットが得られます。 gccとclangは_(int64_t)a / (int32_t)b_のidivを利用していません。おそらく、結果が32ビットレジスタに収まらないと命令が失敗するためです。 1つのdiv から商と剰余を取得することに関するこのQ&Aとは異なり、これはインラインasmの使用例です。 (結果が収まるようにコンパイラーに通知する方法がない限り、idivは失敗しません。)

レジスタにいくつかの引数を配置する呼び出し規約を使用して( right レジスタでもhiを使用)、見た目により近い状況を示しますこのような小さな関数をインライン化するとき。


MSVC

Inline-asmを使用する場合は、register-arg呼び出し規約に注意してください。インラインasmのサポートが不適切に設計/実装されているため、 インラインasmで引数が使用されていない場合、コンパイラはインラインasmの周囲の引数レジスタを保存/復元しない可能性があります 。これを指摘してくれた@RossRidgeに感謝します。

_// MSVC.  Be careful with _vectorcall & inline-asm: see above
// we could return a struct, but that would complicate things
int _vectorcall div64(int hi, int lo, int divisor, int *premainder) {
    int quotient, tmp;
    __asm {
        mov   edx, hi;
        mov   eax, lo;
        idiv   divisor
        mov   quotient, eax
        mov   tmp, edx;
        // mov ecx, premainder   // Or this I guess?
        // mov   [ecx], edx
    }
    *premainder = tmp;
    return quotient;     // or omit the return with a value in eax
}
_

更新:明らかにeaxまたは_edx:eax_に値を残してから、非void関数(returnなし)の終わりから落ちる インライン化されている場合でもサポートされています。これが機能するのは、asmステートメントの後にコードがない場合のみです。 Does __asm {}を参照してください。 eax? の値を返します。これにより、出力(少なくともquotientの場合)のストア/リロードが回避されますが、入力については何もできません。スタック引数を持つ非インライン関数では、それらはすでにメモリ内にありますが、このユースケースでは、便利にインライン化できる小さな関数を書いています。


MSVC 19.00.23026でコンパイルされた_/O2_ on rextester (exeのディレクトリを見つけるmain()および は、コンパイラのasm出力をstdout にダンプします。

_## My added comments use. ##
; ... define some symbolic constants for stack offsets of parameters
; 48   : int ABI div64(int hi, int lo, int divisor, int *premainder) {
    sub esp, 16                 ; 00000010H
    mov DWORD PTR _lo$[esp+16], edx      ## these symbolic constants match up with the names of the stack args and locals
    mov DWORD PTR _hi$[esp+16], ecx

    ## start of __asm {
    mov edx, DWORD PTR _hi$[esp+16]
    mov eax, DWORD PTR _lo$[esp+16]
    idiv    DWORD PTR _divisor$[esp+12]
    mov DWORD PTR _quotient$[esp+16], eax  ## store to a local temporary, not *premainder
    mov DWORD PTR _tmp$[esp+16], edx
    ## end of __asm block

    mov ecx, DWORD PTR _premainder$[esp+12]
    mov eax, DWORD PTR _tmp$[esp+16]
    mov DWORD PTR [ecx], eax               ## I guess we should have done this inside the inline asm so this would suck slightly less
    mov eax, DWORD PTR _quotient$[esp+16]  ## but this one is unavoidable
    add esp, 16                 ; 00000010H
    ret 8
_

余分なmov命令がたくさんあり、コンパイラーはそのいずれかを最適化することすらしません。インラインasm内の_mov tmp, edx_を見て理解し、そのストアをpremainderにすることを考えていました。ただし、インラインasmブロックの前にスタックからレジスタにpremainderをロードする必要があると思います。

この関数は実際には、通常のスタック上のすべてのABIよりも__vectorcall_の方が worse です。レジスタに2つの入力があると、インラインasmが名前付き変数からそれらをロードできるように、それらをメモリに格納します。これがインライン化された場合、さらに多くのパラメーターが正規表現に含まれる可能性があり、それらをすべて格納する必要があるため、asmにメモリオペランドが含まれます。したがって、gccとは異なり、これをインライン化することで得られることはあまりありません。

Asmブロック内で_*premainder = tmp_を実行すると、asmで記述されたより多くのコードを意味しますが、残りの部分の完全に無駄なストア/ロード/ストアパスは回避されます。これにより、命令数が合計2から11に減ります(retは含まれません)。

私はMSVCから可能な限り最高のコードを取得しようとしています。「間違って使用する」のではなく、ストローマン的な議論を作成しています。しかしAFAICTは、非常に短いシーケンスをラップすることは恐ろしいことです。 おそらく64/32-> 32除算用の組み込み関数があり、コンパイラーがこの特定のケースに適したコードを生成できるようにするため、MSVCでこれにインラインasmを使用するという前提はすべて、ストローマン引数。しかし、それは組み込み関数がMSVCのインラインasmよりもずっと優れていることを示しています。


GNU C(gcc/clang/icc)

Gccは、div64をインライン化するときに、ここに示す出力よりも優れています。これは、通常、前のコードが最初にedx:eaxで64ビット整数を生成するように調整できるためです。

32ビットのvectorcall ABI用にgccをコンパイルできません。 Clangはできますが、_"rm"_制約を使用してインラインasmを吸います(godboltリンクで試してください:制約でレジスタオプションを使用する代わりに、メモリを介して関数argをバウンスします)。 64ビットMS呼び出し規約は32ビットvectorcallに近く、最初の2つのパラメーターはedx、ecxにあります。違いは、スタックを使用する前に、さらに2つのパラメーターがregsに入れられることです(呼び出し先はスタックから引数をポップしません。これは、MSVC出力の_ret 8_のことです)。

_// GNU C
// change everything to int64_t to do 128b/64b -> 64b division
// MSVC doesn't do x86-64 inline asm, so we'll use 32bit to be comparable
int div64(int lo, int hi, int *premainder, int divisor) {
    int quotient, rem;
    asm ("idivl  %[divsrc]"
          : "=a" (quotient), "=d" (rem)    // a means eax,  d means edx
          : "d" (hi), "a" (lo),
            [divsrc] "rm" (divisor)        // Could have just used %0 instead of naming divsrc
            // note the "rm" to allow the src to be in a register or not, whatever gcc chooses.
            // "rmi" would also allow an immediate, but unlike adc, idiv doesn't have an immediate form
          : // no clobbers
        );
    *premainder = rem;
    return quotient;
}
_

_gcc -m64 -O3 -mabi=ms -fverbose-asm_ でコンパイル。 -m32を使用すると、3つのロード、idiv、およびストアが取得されます。これは、そのgodboltリンクの変更内容からわかるようにです。

_mov     eax, ecx  # lo, lo
idivl  r9d      # divisor
mov     DWORD PTR [r8], edx       # *premainder_7(D), rem
ret
_

32ビットvectorcallの場合、gccは次のようになります。

_## Not real compiler output, but probably similar to what you'd get
mov     eax, ecx               # lo, lo
mov     ecx, [esp+12]          # premainder
idivl   [esp+16]               # divisor
mov     DWORD PTR [ecx], edx   # *premainder_7(D), rem
ret   8
_

MSVCは、gccの4と比較して、13の命令(retを含まない)を使用します。インライン化を使用すると、前述のように1つにコンパイルされる可能性がありますが、MSVCはおそらく9を使用します(スタックスペースやロードを予約する必要はありません) premainder; 3つの入力のうち約2つを保存する必要があると想定しています。次に、それらをasm内に再読み込みし、idivを実行し、2つの出力を保存し、asmの外部に再読み込みします。つまり、入力用に4つのロード/ストアがあり、出力用にもう4つあります。)

36
Peter Cordes

Gccコンパイラーでは、大きな違いはありません。 asmまたは__asmまたは__asm__も同じです。名前空間の目的の競合を避けるために使用します(asmという名前のユーザー定義関数があります)。

5
oDisPo