私の研究プロジェクトでは、C++コードを書いています。ただし、生成されたアセンブリは、プロジェクトの重要なポイントの1つです。 C++は、特にADC
へのフラグ操作命令への直接アクセスを提供していませんが、コンパイラーがそれを使用できるほどスマートであれば、これは問題にはなりません。考慮してください:
constexpr unsigned X = 0;
unsigned f1(unsigned a, unsigned b) {
b += a;
unsigned c = b < a;
return c + b + X;
}
変数c
は、キャリーフラグを取得してb
とX
に追加するための回避策です。運が良かったようで、(g++ -O3
、バージョン9.1)生成コードは次のとおりです。
f1(unsigned int, unsigned int):
add %edi,%esi
mov %esi,%eax
adc $0x0,%eax
retq
私がテストしたX
のすべての値について、コードは上記のとおりです(もちろん、それに応じて変化する即時値$0x0
は除きます)。ただし、例外が1つ見つかりました:X == -1
(または0xFFFFFFFFu
または~0u
、...実際にはどのように綴るかは関係ありません)の場合、生成されるコードは次のとおりです。
f1(unsigned int, unsigned int):
xor %eax,%eax
add %edi,%esi
setb %al
lea -0x1(%rsi,%rax,1),%eax
retq
これは、間接測定で示唆されているように、最初のコードよりも効率が悪いようです(ただし、あまり科学的ではありません)正しいでしょうか?その場合、これは、報告する価値がありますか?
価値があるのは、clang -O3
、バージョン8.8.0は常にADC
(私が望んだとおり)を使用し、icc -O3
、バージョン19.0.1は使用しないことです。
組み込みの_addcarry_u32
を使用してみましたが、役に立ちませんでした。
unsigned f2(unsigned a, unsigned b) {
b += a;
unsigned char c = b < a;
_addcarry_u32(c, b, X, &b);
return b;
}
_addcarry_u32
を正しく使用していない可能性があると思います(多くの情報を見つけることができませんでした)。キャリーフラグを提供するのは私次第なので、それを使用する意味は何ですか? (再度、c
を導入し、コンパイラが状況を理解できるように祈ります。)
私は実際にはそれを正しく使用しているかもしれません。 X == 0
については、次のように満足しています。
f2(unsigned int, unsigned int):
add %esi,%edi
mov %edi,%eax
adc $0x0,%eax
retq
X == -1
については、私は不満です:-(
f2(unsigned int, unsigned int):
add %esi,%edi
mov $0xffffffff,%eax
setb %dl
add $0xff,%dl
adc %edi,%eax
retq
私はADC
を取得していますが、これは明らかに最も効率的なコードではありません。 (そこではdl
は何をしていますか?キャリーフラグを読み取って復元するための2つの指示?本当に?私は非常に間違っていると思います!)
mov
+ adc $-1, %eax
は、ほとんどのCPUのレイテンシとuopカウントの両方で、xor
- zero + setc
+ 3-component lea
よりも効率的であり、まだ関連するCPUの場合は最悪です。1
これはgccの最適化に失敗したようです:おそらく特殊なケースが発生し、それにラッチして、足元で自分自身を撮影し、adc
パターン認識が行われないようにします。
正確に何を探していたのかわからないので、はい、これを最適化失敗のバグとして報告する必要があります。または、さらに深く掘り下げたい場合は、最適化の合格後にGIMPLEまたはRTLの出力を見て、何が起こるかを確認できます。 GCCの内部表現について何か知っている場合。 Godboltには、「クローンコンパイラ」と同じドロップダウンから追加できるGIMPLEツリーダンプウィンドウがあります。
Clangがadc
を使用してコンパイルするという事実は、それが正当であること、つまり、必要なasmがC++ソースと一致していること、およびコンパイラーがその最適化を停止している特別なケースを見逃していないことを証明します。 (ここでは、clangにバグがないと仮定しています。)
注意しないと、その問題は確かに発生します。 Cでは、キャリーインを受け取り、3入力加算からのキャリーアウトを提供する一般的なadc
関数を記述しようとすると、2つの加算のいずれかでキャリーが発生する可能性があるため、sum < a+b
イディオムは入力の1つに運びます。中央のadc
がキャリーインを実行してキャリーアウトを生成する必要がある場合に、gccまたはclangにadd/adc/adc
を発行させることが可能かどうかはわかりません。
例えば0xff...ff + 1
は0にラップされるため、sum = a+b+carry_in
とcarry_out = sum < a
の特殊なケースで ignore を実行する必要があるため、a = -1
/carry_in = 1
はadc
に最適化できません。
したがって、おそらく別の推測では、gccは+ X
を以前に実行することを検討しており、その特殊なケースのために足で自分自身を撃ったと考えられます。しかし、それはあまり意味がありません。
キャリーフラグを提供するのは私次第なので、それを使用する意味は何ですか?
_addcarry_u32
を正しく使用しています。
その存在のポイントは、キャリー in およびキャリー out で加算を表現できるようにすることです。これは、純粋なCでは困難です。 GCCとclangは、キャリー結果をCFに保持するだけでなく、最適化しません。
キャリーアウトのみが必要な場合は、キャリーインとして0
を指定すると、add
ではなくadc
に最適化されますが、キャリーアウトはC変数として提供されます。
例えば32ビットのチャンクに2つの128ビット整数を追加するには、これを行うことができます
// bad on x86-64 because it doesn't optimize the same as 2x _addcary_u64
// even though __restrict guarantees non-overlap.
void adc_128bit(unsigned *__restrict dst, const unsigned *__restrict src)
{
unsigned char carry;
carry = _addcarry_u32(0, dst[0], src[0], &dst[0]);
carry = _addcarry_u32(carry, dst[1], src[1], &dst[1]);
carry = _addcarry_u32(carry, dst[2], src[2], &dst[2]);
carry = _addcarry_u32(carry, dst[3], src[3], &dst[3]);
}
(GCC/clang/ICCを使用したGodboltの場合)
これは、コンパイラーが64ビットのadd/adcを使用するunsigned __int128
と比べて非常に非効率的ですが、clangとICCにadd
/adc
/adc
/adc
のチェーンを発行させます。 GCCは混乱を招き、setcc
を使用してCFを整数に格納し、次にadd dl, -1
を使用してCFにadc
を戻します。
残念ながら、GCCは、純粋なCで記述された拡張精度/ bigintegerを吸い込みます。Clangは、わずかに優れている場合がありますが、ほとんどのコンパイラーは、これに長けています。これが、ほとんどのアーキテクチャで最低レベルのgmplib関数がasmで手書きされる理由です。
脚注1:またはuopカウントの場合:Intel Haswell以前のバージョンでは、adc
が2 uopsの場合と同じですが、Sandybridge-familyのデコーダーが特別な場合を除いて、即時ゼロは除きます1 uopとして。
しかし、base + index + disp
を使用する3コンポーネントLEAは、Intel CPUで3サイクルのレイテンシ命令になるため、明らかに悪い状態です。
Intel Broadwell以降では、adc
は、0以外の即値でも1-uop命令であり、Haswell for FMAで導入された3入力uopsのサポートを利用しています。
つまり、合計UOPカウントは等しいが、レイテンシが悪いということは、adc
がより良い選択であることを意味します。