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

Cas*_*eri 38 c++ x86 assembly gcc bigint

在我的一个研究项目中,我正在编写C ++代码。但是,生成的程序集是项目的关键点之一。C ++不提供对标志操作指令的直接访问,特别是对C ++,ADC但只要编译器足够聪明地使用它,这就不成问题。考虑:

constexpr unsigned X = 0;

unsigned f1(unsigned a, unsigned b) {
    b += a;
    unsigned c = b < a;
    return c + b + X;
}
Run Code Online (Sandbox Code Playgroud)

Variable c是一种变通方法,可让我掌握进位标志并将其添加到b和中X。看起来我很幸运,(g++ -O3,版本9.1)生成的代码是这样的:

f1(unsigned int, unsigned int):
 add %edi,%esi
 mov %esi,%eax
 adc $0x0,%eax
 retq 
Run Code Online (Sandbox Code Playgroud)

对于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 
Run Code Online (Sandbox Code Playgroud)

这似乎比间接测量所建议的初始代码效率低(虽然不是很科学),对吗?如果是这样,这是否是值得报告的“缺少优化机会”的错误?

对于clang -O38.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;
}
Run Code Online (Sandbox Code Playgroud)

我认为我可能没有_addcarry_u32正确使用(我找不到很多信息)。既然要由我提供进位标志,使用它有什么意义?(再次,介绍c并祈求编译器了解情况。)

实际上,我可能会正确使用它。因为X == 0我很高兴:

f2(unsigned int, unsigned int):
 add %esi,%edi
 mov %edi,%eax
 adc $0x0,%eax
 retq 
Run Code Online (Sandbox Code Playgroud)

因为X == -1我不开心:-(

f2(unsigned int, unsigned int):
 add %esi,%edi
 mov $0xffffffff,%eax
 setb %dl
 add $0xff,%dl
 adc %edi,%eax
 retq 
Run Code Online (Sandbox Code Playgroud)

我确实知道,ADC但这显然不是最有效的代码。(dl在那里做什么?两条指令来读取进位标志并将其恢复?真的吗?我希望我做错了!)

Pet*_*des 33

mov+ adc $-1, %eax比更有效xor-Zero + setc+ 3分量lea的延迟和UOP计数大多数的CPU,以及任何仍然相关的CPU不差。1个


这看起来像是gcc错过的优化:它可能会看到一个特例并锁定该特例,将自己射击在脚上并阻止了adc模式识别的发生。

我不知道它到底在寻找什么/正在寻找什么,所以是的,您应该将其报告为未优化的错误。或者,如果您想更深入地研究自己,可以在优化通过后查看GIMPLE或RTL输出,看看会发生什么。如果您对GCC的内部代表一无所知。Godbolt有一个GIMPLE树转储窗口,您可以从与“克隆编译器”相同的下拉列表中添加。


用clang编译它的事实adc证明它是合法的,即您想要的asm与C ++源代码匹配,并且您没有错过任何阻止编译器执行该优化的特殊情况。(假设clang没有错误,在这里就是这种情况。)

如果您不小心,则肯定会发生此问题,例如,尝试编写一个一般情况下的adc函数,该函数在C中很难实现带有三位输入加法的进位并提供进位,因为这两个加法中的任何一个都可以进行加法运算sum < a+b在将进位添加到输入之一后,您不能只使用惯用语。我不确定是否有可能让gcc或clang发出来add/adc/adc,中间人adc必须随身携带并产生随身携带物品。

例如,0xff...ff + 1回绕为0,因此sum = a+b+carry_in/ carry_out = sum < a无法优化为,adc因为在特殊情况下,其中和需要忽略进位。a = -1carry_in = 1

因此,另一个猜测是,也许gcc考虑过采用+ X更早的方法,并且由于这种特殊情况而自行射击。不过,这没有什么意义。


既然要由我提供进位标志,使用它有什么意义?

您使用_addcarry_u32正确。

其存在的问题是让你表达与进位加法,以及开展,这是很难在纯C. GCC和铛不优化得很好,往往不只是保持在CF的套利结果

如果您只需要结转,可以提供一个0作为结转,它将优化为,add而不是adc,但仍将结转作为C变量给您。

例如,以32位块的形式添加两个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]);
}
Run Code Online (Sandbox Code Playgroud)

关于GCC / clang / ICC的Godbolt

这是非常低效的对比unsigned __int128,其中编译器将只使用64位加法/ ADC,但被打铛和ICC发出一连串add/ adc/ adc/ adc。GCC弄得一团糟,setcc在某些步骤中,add dl, -1它使用CF将其存储为整数,然后将其放回CF中 adc

不幸的是,GCC很讨厌用纯C语言编写的扩展精度/biginteger。Clang有时会稍好一些,但是大多数编译器都不擅长。这就是为什么对于大多数体系结构,最低级别的gmplib函数都是在asm中手写的原因。


脚注1:或用于uop计数:在Intel Haswell和更早的版本中等于adc2 oups,但零零立即数(Sandybridge-family的解码器特例为1 uop)除外。

但是带有3个成分的LEA base + index + disp使其在Intel CPU上具有3个周期的延迟指令,因此肯定更糟。

在Intel Broadwell及更高版本上,adc即使是非零立即数,它也是1 uop指令,它利用了Haswell为FMA引入的3输入uops的支持。

因此,相等的总uop计数,但更糟的延迟意味着那adc将仍然是一个更好的选择。

https://agner.org/optimize/

  • 非常感谢。+1也用于`_addcarry_u32`的解释。我的真实案例(就像我的帖子一样)关注的是“ ADD”,其次是“ ADC”,其中“ ADD”之后的进位很重要,而“ ADC”之后的进位则不重要。因此,我没有注意后者,而未能理解产生进位的`_addcarry_u32'的关键点。现在,由于您的帖子,`_addcarry_u32`对我来说非常有意义。我会记住这一点。我有种感觉,我可能很快就需要,您可能使我免于再次头疼。 (2认同)