C++コードの非常に内側のループで128ビット整数カウンターを使用しています。 (不適切な背景:実際のアプリケーションは、通常のグリッドで有限差分方程式を評価します。これには、大きな整数を繰り返しインクリメントすることが含まれます。小さな丸めは回答に影響を与えるほど蓄積されるため、64ビットでも十分な精度ではありません。)
整数を2つの64ビットunsignedlongとして表しました。これらの値を128ビット定数でインクリメントする必要があります。これは難しいことではありませんが、ローワードからハイワードへのキャリーを手動でキャッチする必要があります。
私は次のような作業コードを持っています:
inline void increment128(unsigned long &hiWord, unsigned long &loWord)
{
const unsigned long hiAdd=0x0000062DE49B5241;
const unsigned long loAdd=0x85DC198BCDD714BA;
loWord += loAdd;
if (loWord < loAdd) ++hiWord; // test_and_add_carry
hiWord += hiAdd;
}
これはタイトでシンプルなコードです。できます。
残念ながら、これは私の実行時間の約20%です。キラーラインはそのloWordテストです。それを削除すると、明らかに間違った答えが返されますが、実行時のオーバーヘッドは20%から4%に低下します。そのため、キャリーテストは特に高価です!
私の質問:C++は、GCCの拡張機能としても、ハードウェアキャリーフラグを公開しますか?実際にコンパイルされた命令がhiWordの加算に最後のキャリー命令を使用した加算を使用した場合、上記のtest-and-add-carry行なしで加算を実行できるようです。 test-and-add-carry行を書き直して、コンパイラーに組み込みオペコードを使用させる方法はありますか?
実際、コードを注意深く書くと、gccは自動的にキャリーを使用します...
現在のGCCは、hiWord += (loWord < loAdd);
をadd
/ adc
(x86のadd-with-carry)に最適化できます。 この最適化はGCC5.3で導入されました。
uint64_t
チャンクを使用する場合: https://godbolt.org/z/S2kGRz 。uint32_t
チャンクを使用した32ビットモードでも同じことが言えます: https://godbolt.org/z/9FC9vc(編集者注:もちろん難しい部分は、キャリーインとキャリーアウトを備えた正しい全加算器を書くことです;それはCでは難しいことであり、GCCはそれを最適化する方法を知りません私は見た。)
関連項目: https://gcc.gnu.org/onlinedocs/gcc/Integer-Overflow-Builtins.html は、符号なしまたは符号付きオーバーフローの検出からの実行を提供できます。
GCC4.5のような古いGCCは、setc
を使用する代わりに、追加からのキャリーアウトで分岐またはadc
し、adc
(add-with-キャリー)フラグを付けます-__int128
を使用した場合はadd
の結果。 (または、32ビットターゲットではuint64_t
)。 gccに128ビット整数はありますか? -GCC4.1以降でサポートされている64ビットターゲットでのみ参照してください。
私はこのコードをgcc -O2 -Wall -Werror -S
でコンパイルしました:
void increment128_1(unsigned long &hiWord, unsigned long &loWord)
{
const unsigned long hiAdd=0x0000062DE49B5241;
const unsigned long loAdd=0x85DC198BCDD714BA;
loWord += loAdd;
if (loWord < loAdd) ++hiWord; // test_and_add_carry
hiWord += hiAdd;
}
void increment128_2(unsigned long &hiWord, unsigned long &loWord)
{
const unsigned long hiAdd=0x0000062DE49B5241;
const unsigned long loAdd=0x85DC198BCDD714BA;
loWord += loAdd;
hiWord += hiAdd;
hiWord += (loWord < loAdd); // test_and_add_carry
}
これはincrement128_1のアセンブリです。
.cfi_startproc
movabsq $-8801131483544218438, %rax
addq (%rsi), %rax
movabsq $-8801131483544218439, %rdx
cmpq %rdx, %rax
movq %rax, (%rsi)
ja .L5
movq (%rdi), %rax
addq $1, %rax
.L3:
movabsq $6794178679361, %rdx
addq %rdx, %rax
movq %rax, (%rdi)
ret
...そしてこれはincrement128_2のアセンブリです:
movabsq $-8801131483544218438, %rax
addq %rax, (%rsi)
movabsq $6794178679361, %rax
addq (%rdi), %rax
movabsq $-8801131483544218439, %rdx
movq %rax, (%rdi)
cmpq %rdx, (%rsi)
setbe %dl
movzbl %dl, %edx
leaq (%rdx,%rax), %rax
movq %rax, (%rdi)
ret
2番目のバージョンには条件分岐がないことに注意してください。
[編集]
また、GCCはエイリアシングについて心配する必要があるため、参照はパフォーマンスに悪影響を与えることがよくあります...値で渡す方がよい場合がよくあります。考えてみましょう:
struct my_uint128_t {
unsigned long hi;
unsigned long lo;
};
my_uint128_t increment128_3(my_uint128_t x)
{
const unsigned long hiAdd=0x0000062DE49B5241;
const unsigned long loAdd=0x85DC198BCDD714BA;
x.lo += loAdd;
x.hi += hiAdd + (x.lo < loAdd);
return x;
}
アセンブリ:
.cfi_startproc
movabsq $-8801131483544218438, %rdx
movabsq $-8801131483544218439, %rax
movabsq $6794178679362, %rcx
addq %rsi, %rdx
cmpq %rdx, %rax
sbbq %rax, %rax
addq %rcx, %rax
addq %rdi, %rax
ret
これは実際には3つの中で最も厳しいコードです。
... OKなので、実際にキャリーを自動的に使用したものはありません:-)。しかし、条件分岐は避けています。これは遅い部分だと思います(分岐予測ロジックは半分の時間で間違ってしまうため)。
[編集2]
そしてもう1つ、ちょっとした検索をしているときに偶然見つけました。 GCCには128ビット整数のサポートが組み込まれていることをご存知ですか?
typedef unsigned long my_uint128_t __attribute__ ((mode(TI)));
my_uint128_t increment128_4(my_uint128_t x)
{
const my_uint128_t hiAdd=0x0000062DE49B5241;
const unsigned long loAdd=0x85DC198BCDD714BA;
return x + (hiAdd << 64) + loAdd;
}
これのためのアセンブリはそれが得るのとほぼ同じくらい良いです:
.cfi_startproc
movabsq $-8801131483544218438, %rax
movabsq $6794178679361, %rdx
pushq %rbx
.cfi_def_cfa_offset 16
addq %rdi, %rax
adcq %rsi, %rdx
popq %rbx
.cfi_offset 3, -16
.cfi_def_cfa_offset 8
ret
(ebx
のプッシュ/ポップがどこから来たのかはわかりませんが、それでも悪くはありません。)
ちなみに、これらはすべてGCC4.5.2を使用しています。
もちろん、最善の答えは、組み込みの__int128_t
サポートを使用することです。
または、インラインasmを使用します。名前付き引数形式を使用することを好みます。
__asm("add %[src_lo], %[dst_lo]\n"
"adc %[src_hi], %[dst_hi]"
: [dst_lo] "+&r" (loWord), [dst_hi] "+r" (hiWord)
: [src_lo] "erm" (loAdd), [src_hi] "erm" (hiAdd)
: );
loWord
は、他のいくつかのオペランドが読み取られる前に書き込まれるため、 early clobber オペランドとしてフラグが付けられます。これにより、gccが同じレジスタを使用して両方を保持するのを防ぐため、hiAdd = loWord
の間違ったコードを回避できます。ただし、安全な場合は、コンパイラがloAdd = loWord
の場合に同じレジスタを使用するのを防ぎます。
その初期のクラバーの質問が指摘しているように、インラインasmは本当に間違ってしまいがちです(インライン化されたコードに何らかの変更を加えた後にのみ問題を引き起こすデバッグが難しい方法で)。
x86およびx86-64インラインasmはフラグを上書きすると想定されているため、明示的な「cc」クローバーは必要ありません。