web-dev-qa-db-ja.com

ADCに関して-1(0xFFFFFFFF)について何か特別なことはありますか?

私の研究プロジェクトでは、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は、キャリーフラグを取得してbXに追加するための回避策です。運が良かったようで、(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つの指示?本当に?私は非常に間違っていると思います!)

38
Cassio Neri

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_incarry_out = sum < aの特殊なケースで ignore を実行する必要があるため、a = -1/carry_in = 1adcに最適化できません。

したがって、おそらく別の推測では、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がより良い選択であることを意味します。

https://agner.org/optimize/

33
Peter Cordes