关于ADC -1(0xFFFFFFFF)有什么特别之处吗?

时间:2019-05-12 17:03:03

标签: c++ gcc assembly x86 bigint

在我的一个研究项目中,我正在编写C ++代码。但是,生成的程序集是项目的关键点之一。 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会相应变化)。但是,我发现了一个例外:当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

这个项目使我发疯,更具体地说,是我发现的所有与我的代码无关的问题都使我发疯。列举一些:

  1. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=88797
  2. 我收支平衡了:https://github.com/mattgodbolt/compiler-explorer/issues/1377

:-(

1 个答案:

答案 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,因为在特殊情况下需要忽略 adca = -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仍然是更好的选择。

https://agner.org/optimize/