我在我的C ++代码的内部循环中使用了128位整数计数器。 (不相关的背景:实际的应用是评估规则网格上的有限差分方程,它涉及重复递增大整数,甚至64位也不够精确,因为小的舍入累积到足以影响答案。)
我将整数表示为两个64位无符号长整数。我现在需要将这些值递增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%。这条杀手线就是低价测试。如果我删除它,我显然得到了错误的答案,但运行时开销从20%下降到4%!所以携带测试特别贵!
我的问题:C ++是否公开了硬件进位标志,即使是作为GCC的扩展? 如果实际编译的指令使用最后一个进位指令进行添加,似乎可以在没有上面的测试和添加进位线的情况下完成添加。 有没有办法重写test-and-add-carry行以使编译器使用内部操作码?
答案 0 :(得分:38)
实际上,如果你仔细编写代码,gcc会自动使用进位......
我用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
请注意第二版中缺少条件分支。
[编辑]
此外,引用通常对性能不利,因为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
这实际上是三者中最严密的代码。
......好的,所以他们都没有自动使用随身携带:-)。但他们确实避免了条件分支,我敢打赌它是缓慢的部分(因为分支预测逻辑会在一半时间内将其错误)。
[编辑2]
还有一个,我偶然发现了一点点。你知道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
的推/弹来自何处,但这仍然不错。)
顺便说一下,所有这些都是GCC 4.5.2。
答案 1 :(得分:16)
当然,最好的答案是使用内置的__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操作数,因为它是在读取其他一些操作数之前编写的。这避免了hiAdd = loWord
的错误代码,因为它会阻止gcc使用相同的寄存器来保存两者。它会阻止编译器为loAdd = loWord
情况使用相同的寄存器,但它是安全的。
正如早期的问题所指出的那样,内联asm很容易出错(以难以调试的方式,只会在对内联代码进行一些更改后导致麻烦)。
假设x86和x86-64内联asm会破坏标志,因此不需要显式的“cc”clobber。