48

__asm { ... };私が知る限り、との唯一の違い__asm__("...");は、最初の使用mov eax, varと2番目の使用movl %0, %%eax:"=r" (var)最後にあることです。他にどのような違いがありますか?そして、どうasmですか?

4

4 に答える 4

49

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

パフォーマンス上の理由でインライン asm を使用している場合、インライン関数で短いシーケンスをラップする場合ではなく、ループ全体を asm で完全に記述する場合にのみ、MSVC インライン asm が実行可能になります。以下の例 (関数でラップidiv) は、MSVC が苦手な種類のものです: ~8 余分なストア/ロード命令。

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

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

GNU C インライン asmは、 asm を学習する良い方法ではありません。コードについてコンパイラーに伝えることができるように、asm をよく理解する必要があります。そして、コンパイラが何を知る必要があるかを理解する必要があります。その回答には、他の inline-asm ガイドおよび Q&A へのリンクもあります。タグのwiki には、一般的に asm に関する優れたものがたくさんありますが、GNU インライン asm については、そこへのリンクのみです。(その回答の内容は、x86 以外のプラットフォームの GNU インライン asm にも適用されます。)

GNU C インライン asm 構文は、gcc、clang、icc、および GNU C を実装する一部の商用コンパイラで使用されます。

  • 何を壊すかをコンパイラに伝える必要があります。これを怠ると、デバッグが困難な方法で周囲のコードが破損する可能性があります。
  • 強力ですが、入力の提供方法と出力の場所をコンパイラに指示するための構文を読み、学習し、使用するのは困難です。たとえば、インライン asm を実行する前に"c" (shift_count)コンパイラに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 はidivforを利用しません(int64_t)a / (int32_t)b。おそらく、結果が 32 ビット レジスタに収まらない場合に命令が失敗するためです。したがって、 one からの商と剰余の取得に関するこの Q&Adivとは異なり、これはインライン asm の使用例です。(結果が適合することをコンパイラに通知する方法がない限り、idiv は失敗しません。)

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


MSVC

inline-asm を使用する場合は、register-arg 呼び出し規約に注意してください。どうやら inline-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
}

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


rextester で MSVC 19.00.23026/O2 使用してコンパイルされました( main()exe のディレクトリを検出し、コンパイラの asm 出力を stdout にダンプするa を使用)。

## 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 命令があり、コンパイラはそれを最適化することさえできません。mov tmp, edxインライン asm の内部を見て理解し、それをストアにするのではないかと考えましたpremainder。しかし、それには、インライン asm ブロックの前にスタックからレジスターにロードする必要があるpremainderと思います。

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

asm ブロック内で行う*premainder = tmpということは、より多くのコードが asm で記述されることを意味しますが、残りの完全に脳死状態のストア/ロード/ストア パスを回避します。これにより、命令数が合計 2 つ減り、11 になります ( を除くret)。

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


GNU C (gcc/clang/icc)

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

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

// 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 を使用すると、godbolt リンクの内容を変更することからわかるように、3 つのロード、idiv、およびストアを取得するだけです。

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

gcc の 4 命令と比較して、MSVC は 13 命令 (ret を含まない) を使用します。先ほど述べたように、インライン化を使用すると、1 つだけにコンパイルされる可能性がありますが、MSVC はまだおそらく 9 命令を使用します。premainder; 3 つの入力のうち約 2 つをまだ保存する必要があると仮定しています. 次に、それらを asm 内にリロードし、 を実行しidiv、2 つの出力を保存し、asm の外にそれらをリロードします. つまり、入力用に 4 回のロード/ストア、および別の 4 回のロード/ストアです。出力用です。)

于 2016-03-12T15:53:58.853 に答える
15

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

于 2010-07-24T01:33:31.333 に答える
5

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

于 2012-06-07T20:26:44.670 に答える