在我的一个研究项目中,我正在编写C ++代码。但是,生成的程序集是项目的关键点之一。 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
会相应变化)。但是,我发现了一个例外:当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
在那里做什么?两条指令来读取进位标志并将其恢复?真的吗?我希望我做错了!)
我的怒吼仍在继续...(对不起,即使出于治疗原因,我也需要与某人分享)
当然,对我而言,X
的唯一值是-1
,正因为如此,我可能不得不使用一些内联asm
。
这个项目使我发疯,更具体地说,是我发现的所有与我的代码无关的问题都使我发疯。列举一些:
:-(
答案 0 :(得分:33)
mov
+ adc $-1, %eax
的延迟和uop计数比xor
-零+ setc
+ 3分量lea
更有效 1
这似乎是gcc错过的优化:它可能会看到一个特例并锁定该特例,将自己开枪射击并阻止adc
模式识别的发生。
我不知道它到底在寻找什么/正在寻找什么,所以是的,您应该将此报告为未优化优化错误。或者,如果您想更深入地研究自己,可以在优化通过后查看GIMPLE或RTL输出,看看会发生什么。如果您对GCC的内部代表一无所知。 Godbolt有一个GIMPLE树转储窗口,您可以从与“克隆编译器”相同的下拉列表中添加。
使用adc
进行clang编译的事实证明这是合法的,即您想要的asm确实与C ++源代码匹配,并且您不会错过某些阻止编译器执行该优化的特殊情况。 (假设clang没有错误,在这里就是这种情况。)
如果您不小心,例如,可能会发生该问题。在C语言中,很难编写一种一般情况下的adc
函数,该函数可以带进位并提供3输入加法的进位,因为这两个加法中的任何一个都可以随身携带,所以您不能只使用{将进位加到输入之一后的{1}}惯用语。我不确定是否有可能让gcc或clang发出sum < a+b
,而中间的add/adc/adc
必须携带进位并产生进位。
例如adc
绕回为0,因此0xff...ff + 1
/ sum = a+b+carry_in
无法优化为carry_out = sum < a
,因为在特殊情况下需要忽略 adc
和a = -1
。
因此,另一个猜测是,也许gcc考虑过更早地使用carry_in = 1
,并且由于这种特殊情况而将自己开枪了。不过,这没有什么意义。
使用它有什么意义,因为要由我提供进位标志吗?
您正确使用了+ X
。
它的存在是要让您用进位 in 和进位 out 表示加法,这在纯C语言中很难实现。不能很好地优化它,通常不只是将进位结果保存在CF中。
如果您只想结转,可以提供_addcarry_u32
作为结转,它将优化为0
而不是add
,但仍然可以结转作为C变量。
例如在32位块中添加两个128位整数,您可以执行此操作
adc
( On Godbolt with GCC/clang/ICC )
与// 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]);
}
相比,效率非常低,在unsigned __int128
中,编译器仅使用64位add / adc,但确实使clang和ICC发出add
/ adc
/ {{ 1}} / adc
。 GCC弄得一团糟,使用adc
将CF存储为整数以执行某些步骤,然后使用setcc
将其放回CF中进行add dl, -1
。
不幸的是,GCC很讨厌用纯C语言编写的扩展精度/biginteger。Clang有时会稍好一些,但大多数编译器都不好。这就是为什么对于大多数体系结构,最低级别的gmplib函数都是在asm中手写的原因。
脚注1 :或用于uop计数:在Intel Haswell及更早版本中,adc
等于2 oups,但零零表示Sandybridge-family解码器的特殊情况为1 uop
但是带有adc
的3分量LEA使其成为Intel CPU上的3周期延迟指令,因此肯定更糟。
在Intel Broadwell及更高版本上,base + index + disp
甚至是立即数非零的1 uop指令,它利用了Haswell为FMA引入的3输入微指令的支持。
因此,总的uop数量相等,但延迟更短,这意味着adc
仍然是更好的选择。