5

分岐を排除するためにコードの最適化を実行しようとしています。元の C コードは次のとおりです。

if( a < b ) 
   k = (k<<1) + 1;
else
   k = (k<<1)

以下のようなアセンブリコードに置き換えるつもりです

mov a, %rax 
mov b, %rbx
mov k, %rcx
xor %rdx %rdx
shl 1, %rcx
cmp %rax, %rax
setb %rdx
add %rdx,%rcx
mov %rcx, k 

だから私はblowのようなcインラインアセンブリコードを書きます、

#define next(a, b, k)\
 __asm__("shl $0x1, %0; \
         xor %%rbx, %%rbx; \
         cmp %1, %2; \
         setb %%rbx; \
         addl  %%rbx,%0;":"+c"(k) :"g"(a),"g"(b))

以下のコードをコンパイルすると、エラーが発生しました。

operand type mismatch for `add'
operand type mismatch for `setb'

どうすれば修正できますか?

4

4 に答える 4

10

コードの間違いは次のとおりです。

  1. エラー: 'cmp' のオペランド タイプが一致しません-- CMPのオペランドの 1 つはレジスタでなければなりません。2 つの即値を比較しようとするコードを生成している可能性があります。2 番目のオペランドの制約を"g"から"r"に変更します。( GCC マニュアル - Extended Asm - Simple Constraintsを参照)
  2. エラー: 'setb' のオペランド タイプが一致しません-- SETBは 8 ビット オペランドしか使用できません。つまり、 setb %bl機能setb %rbxしないのに機能します。
  3. C 式は、AT&T x86 アセンブラー構文にT = (A < B)変換する必要があります。CMPcmp B,A; setb Tへの 2 つのオペランドの順序が間違っていました。CMPはSUBのように機能することに注意してください。

最初の 2 つのエラー メッセージがアセンブラーによって生成されたものであることがわかったら、これらをデバッグするコツは、gcc によって生成されたアセンブラー コードを調べることです。問題のある行をx86 オペコード リファレンスgcc $CFLAGS -S t.cと比較してみてください。各命令で許可されているオペランド コードに注目すると、問題がすぐにわかります。t.s

以下に投稿された修正済みのソース コードでは、 SETLの代わりにSETBを使用しているため、オペランドは符号なしであると想定しています。RCXは ABI で破壊されたレジスタを呼び出すため、一時的な値を保持するためにRBXの使用からRCXに切り替え、RCX は入力の前にクリアされて読み取られるため、制約を使用して早い段階で破壊されたオペランドとしてマーク しまし"=&c"ab

#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

static uint64_t next(uint64_t a, uint64_t b, uint64_t k)
{
    uint64_t tmp;
    __asm__("shl $0x1, %[k];"
        "xor %%rcx, %%rcx;"
        "cmp %[b], %[a];"
        "setb %%cl;"
        "addq %%rcx, %[k];"
        : /* outputs */ [k] "+g" (k), [tmp] "=&c" (tmp)
        : /* inputs  */ [a] "r" (a), [b] "g" (b)
        : /* clobbers */ "cc");
    return k;
}

int main()
{
    uint64_t t, t0, k;
    k = next(1, 2, 0);
    printf("%" PRId64 "\n", k);

    scanf("%" SCNd64 "%" SCNd64, &t, &t0);
    k = next(t, t0, k);
    printf("%" PRId64 "\n", k);

    return 0;
}

main()は次のように変換されます。

<+0>:   push   %rbx
<+1>:   xor    %ebx,%ebx
<+3>:   mov    $0x4006c0,%edi
<+8>:   mov    $0x1,%bl
<+10>:  xor    %eax,%eax
<+12>:  sub    $0x10,%rsp
<+16>:  shl    %rax
<+19>:  xor    %rcx,%rcx
<+22>:  cmp    $0x2,%rbx
<+26>:  setb   %cl
<+29>:  add    %rcx,%rax
<+32>:  mov    %rax,%rbx
<+35>:  mov    %rax,%rsi
<+38>:  xor    %eax,%eax
<+40>:  callq  0x400470 <printf@plt>
<+45>:  lea    0x8(%rsp),%rdx
<+50>:  mov    %rsp,%rsi
<+53>:  mov    $0x4006c5,%edi
<+58>:  xor    %eax,%eax
<+60>:  callq  0x4004a0 <__isoc99_scanf@plt>
<+65>:  mov    (%rsp),%rax
<+69>:  mov    %rbx,%rsi
<+72>:  mov    $0x4006c0,%edi
<+77>:  shl    %rsi
<+80>:  xor    %rcx,%rcx
<+83>:  cmp    0x8(%rsp),%rax
<+88>:  setb   %cl
<+91>:  add    %rcx,%rsi
<+94>:  xor    %eax,%eax
<+96>:  callq  0x400470 <printf@plt>
<+101>: add    $0x10,%rsp
<+105>: xor    %eax,%eax
<+107>: pop    %rbx
<+108>: retq   

各コールの前にRSInext()に移動した結果を確認できます。printf()

于 2012-12-24T17:37:59.727 に答える
9

gcc (および gcc インライン アセンブラーのように見える) が生成するとします。

leal    (%rdx,%rdx), %eax
xorl    %edx, %edx
cmpl    %esi, %edi
setl    %dl
addl    %edx, %eax
ret

から

int f(int a, int b, int k)
{
  if( a < b ) 
    k = (k<<1) + 1;
  else
    k = (k<<1);

  return k;
}

独自のインライン アセンブラを作成することは、時間と労力の完全な無駄であると考えるでしょう。

いつものように、インライン アセンブラを書き始める前に、コンパイラが実際に何をするかを確認してください。コンパイラがこのコードを生成しない場合は、コンパイラのバージョンを少し新しいものにアップグレードする必要があるかもしれません (この種のことを Jan Hubicka [当時の x86-64 の gcc メンテナー] ca 2001 に報告しました。私はそれがかなり長い間gccにあったと確信しています)。

于 2012-12-24T15:36:38.970 に答える
2

概要:

  • ブランチレスは最良の選択ではないかもしれません。
  • インライン asm は他のいくつかの最適化を無効にします。最初に他のソースの変更を試してください。たとえば? :、分岐なしでコンパイルすることが多く、整数 0/1 としてブール値も使用します。
  • inline-asm を使用する場合は、asm ブロックの外側でコンパイラが生成するコードを効率的にするために、制約も最適化してください。
  • 全体はcmp %[b], %[a]/で実行可能adc %[k],%[k]です。 手書きのコードはコンパイラが生成するものよりも悪いですが、定数伝播/ CSE /インライン化によってこのコードが(部分的に)最適化されなかった場合、小規模では打ち負かされます。

コンパイラが分岐コードを生成し、プロファイリングがそれが間違った選択であったことを示す場合 (Linux の && などで、その命令での分岐ミスのカウントが多い)perf record -ebranch-misses ./my_programperf reportはい、分岐のないコードを取得するために何かをする必要があります。

(分岐は、予測可能であれば利点となる可能性があります。分岐とは、使用するコードの順不同の実行を意味し、待機して準備が整う必要はありません。LLVM は最近(k<<1) + 1x86 コード生成をデフォルトより分岐させるパッチをマージしました。最新の x86 CPU には非常に強力な分岐予測機能があるため. Clang/LLVM ナイトリー ビルド (そのパッチを適用) は、少なくともループ外のスタンドアロン関数では、この C ソースに対して依然として分岐なしを選択します)。ab

これがバイナリ検索の場合、同じ検索が頻繁に行われない限り、ブランチレスはおそらく良い戦略です。(分岐 + 投機的実行は、クリティカル パスから離れた制御依存関係があることを意味します。

プロファイルに基づく最適化を使用してコンパイルすると、ほとんどの場合、どの分岐が一方通行になるかに関するランタイム情報がコンパイラーに提供されます。予測が不十分な分岐と、全体的に両方のパスをたどるが単純なパターンを持つ分岐との違いをまだ認識していない可能性があります。(または、グローバル ヒストリに基づいて予測可能です。最新のブランチ プレディクタ デザインの多くは、ブランチ ヒストリに基づいてインデックスを作成するため、最後のいくつかのブランチがどのように進んだかによって、現在のブランチに使用されるテーブル エントリが決まります。)

関連: gcc 最適化フラグ -O3 はコードを遅くします -O2は、並べ替えられた配列がループ内の条件に対してほぼ完全な分岐予測を行い、gcc -O3(プロファイルに基づく最適化を使用しない) 分岐のないコードがデータ依存性のボトルネックになるケースを示します。使用からcmov。しかし-O3 -fprofile-use、分岐コードを作成します。(また、それを別の方法で記述すると、自動ベクトル化も向上する低レイテンシーのブランチレス コードが作成されます。)


(k<<1) + (a<b)インライン asm は、たとえば、他の人が提案したように書くことによって、必要な asm を作成するためにコンパイラを手に入れることができない場合、最後の手段にする必要があります。

インラインasmは、多くの最適化、最も明白な定数伝播を打ち負かします(gccが定数をインラインasmコードのブロック外のレジスタに移動する他の回答に見られるように)。 https://gcc.gnu.org/wiki/DontUseInlineAsm .

コンパイラが一部/すべての変数に対して定数値を持っている場合、純粋な C バージョンを使用するためになどを使用することもできますがif(__builtin_constant_p(a))、それはより多くの作業です。__builtin_constant_p()(また、関数のインライン化の前に評価されるClang ではうまく機能しません。)

それでも(入力がコ​​ンパイル時の定数ではない場合に限定すると)、コンパイラに全範囲のオプションを与えることはできません。状況に応じて異なる命令を使用したい場合、あなたはうんざりしますが、ここでは多代替制約を使用して柔軟性のほとんどを明らかにすることができaますbcmp

通常は、インライン asm を使用するよりも、コンパイラに最適に近いコードを作成させる方が適切です。Inline-asm は、一時的な結果を再利用したり、命令を分散させて他のコンパイラ生成コードと混合したりするコンパイラの機能を破壊します。(命令のスケジューリングは、x86 では順不同で実行されるため大した問題ではありませんが、それでもなお.)


そのasmはかなりがらくたです。分岐ミスが多い場合は、分岐実装よりも優れていますが、分岐のない実装の方がはるかに優れています。

あなたa<bは署名されていない比較です(あなたは、署名されsetbていない以下の条件を使用しています)。したがって、比較結果はキャリーフラグにあります。x86 には add-with-carry 命令があります。なお、k<<1は と同じものですk+k

したがって、必要な asm (コンパイラ生成またはインライン asm を使用) は次のとおりです。

# k in %rax,    a in %rdi,  b in %rsi   for this example
cmp     %rsi, %rdi      # CF = (a < b) = the carry-out from edi - esi
adc     %rax, %rax      # eax = (k<<1) + CF  = (k<<1) + (a < b)

コンパイラはaddorleaを 1 だけ左シフトするのに十分に賢く、一部はadcの代わりに使用できるほど賢くsetb、両方を組み合わせることができません。

レジスター引数と戻り値を使用して関数を作成することは、多くの場合、コンパイラーが何を行うかを確認する良い方法ですが、別のレジスターで結果を生成することを強制します。(こちらの Q&Aと Matt Godbolt の CppCon2017 トークも参照してください: 「私のコンパイラは最近何をしてくれましたか? コンパイラのふたを外す」</a>)。

// I also tried a version where k is a function return value,
// or where k is a global, so it's in the same register.
unsigned funcarg(unsigned a, unsigned b, unsigned k) {
    if( a < b ) 
       k = (k<<1) + 1;
    else
       k = (k<<1);
    return k;
}

Godbolt コンパイラーの explorerと、他のいくつかのバージョン。(unsignedこのバージョンで使用したaddlのは、asm にあったためです。使用unsigned longすると、xor-zeroing 以外のすべてが 64 ビット レジスタになります。(xor %eax,%eaxこれは、RAX をゼロにするための最良の方法です。)

 # gcc7.2 -O3  When it can keep the value in the same reg, uses add instead of lea
    leal    (%rdx,%rdx), %eax       #, <retval>
    cmpl    %esi, %edi      # b, a
    adcl    $0, %eax        #, <retval>
    ret

#clang 6.0 スナップショット -O3 xorl %eax, %eax cmpl %esi, %edi setb %al leal (%rax,%rdx,2), %eax retq

# ICC18、gcc と同じだが MOV の保存に失敗する addl %edx, %edx #14.16 cmpl %esi, %edi #17.12 adcl $0, %edx #17.12 movl %edx, %eax #17.12 ret #17.12

MSVC は、手持ちなしではブランチレス コードを作成できない唯一のコンパイラです。(上記の clang(k<<1) + ( a < b );とまったく同じxor/ cmp/ setb/leaシーケンスが得られます (ただし、Windows x86-64 呼び出し規約を使用します)。

funcarg PROC                         ; x86-64 MSVC CL19 -Ox
    lea      eax, DWORD PTR [r8*2+1]
    cmp      ecx, edx
    jb       SHORT $LN3@funcarg
    lea      eax, DWORD PTR [r8+r8]   ; conditionally jumped over
$LN3@funcarg:
    ret      0

インライン asm

他の回答は、実装の問題をかなりうまくカバーしています。インライン asm でアセンブラー エラーをデバッグするには、asm テンプレートが埋められた状態で、コンパイラーがアセンブラーに何を供給しているかを確認するために使用gcc -O3 -S -fverbose-asmaddl %rax, %ecxします。

この最適化された実装では、複数の代替制約を使用して、コンパイラがcmp $imm, r/mcmp r/m, r、またはcmp r, r/mCMP のいずれかの形式を選択できるようにします。オペコードではなく、可能なメモリオペランドがどちら側に含まれているかによって物事を分割する2つの代替手段を使用しました。 "rme"(rmi)に似"g"ていますが、32 ビット符号拡張即値に限定されます)。

unsigned long inlineasm(unsigned long a, unsigned long b, unsigned long k)
{
    __asm__("cmpq %[b], %[a]   \n\t"
            "adc %[k],%[k]"
        : /* outputs */ [k] "+r,r" (k)
        : /* inputs  */ [a] "r,rm" (a), [b] "rme,re" (b)
        : /* clobbers */ "cc");  // "cc" clobber is implicit for x86, but it doesn't hurt
    return k;
}

これを、さまざまなコンテキストでインライン化する呼び出し元と共に Godbolt に配置しました。gcc7.2-O3は、スタンドアロン バージョン (レジスタ引数を使用) に期待することを行います。

inlineasm:
    movq    %rdx, %rax      # k, k
    cmpq %rsi, %rdi         # b, a
    adc %rax,%rax   # k
    ret

他の呼び出し元にインライン化することで、制約がどの程度うまく機能するかを確認できます。

unsigned long call_with_mem(unsigned long *aptr) {
    return inlineasm(*aptr, 5, 4);
}
    # gcc
    movl    $4, %eax        #, k
    cmpq $55555, (%rdi)     #, *aptr_3(D)
    adc %rax,%rax   # k
    ret

より大きな即値でmovabs、レジスターに入ります。(ただし"i"or"g"制約があると、gcc はアセンブルしないコードを出力するか、定数を切り捨てて、cmpq に大きな即値定数を使用しようとします。)

純粋な C から得られるものを比較します。

unsigned long call_with_mem_nonasm(unsigned long *aptr) {
    return handhold(*aptr, 5, 4);
}
    # gcc -O3
    xorl    %eax, %eax      # tmp93
    cmpq    $4, (%rdi)      #, *aptr_3(D)
    setbe   %al   #, tmp93
    addq    $8, %rax        #, k
    ret

adc $8, %raxなしsetcのほうがよかったかもしれませんが、__builtin_constant_p()onなしではインライン asm からそれを得ることができませんk


clang は、mem の代替があればそれを選択することが多いため、次のようにします: /facepalm. インライン asm を使用しないでください。

inlineasm:   # clang 5.0
    movq    %rsi, -8(%rsp)
    cmpq    -8(%rsp), %rdi
    adcq    %rdx, %rdx
    movq    %rdx, %rax
    retq

ところで、比較加算へのシフトを最適化するつもりがない限り、コンパイラにk<<1入力として要求することができますし、そうすべきでした。

于 2017-11-29T23:11:32.090 に答える