在我的一个研究项目中,我正在编写 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
会相应地改变)。不过我发现了一个异常(exception):当 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
在那里做什么?两条指令读取进位标志并恢复它?真的吗?我希望我错了!)
最佳答案
mov
+ adc $-1, %eax
比xor
更有效率-零 + setc
+ 三组分 lea
对于大多数 CPU 的延迟和 uop 计数,并且在任何仍然相关的 CPU 上都没有更糟。1
这看起来像是 gcc 错过了优化 :它可能看到了一个特殊情况并锁定在它上面,用脚射击并防止 adc
模式识别发生。
我不知道它到底看到了什么/正在寻找什么,所以是的,您应该将此报告为一个错过的优化错误。或者,如果您想自己深入挖掘,可以在优化通过后查看 GIMPLE 或 RTL 输出,看看会发生什么。如果您对 GCC 的内部表示有所了解。 Godbolt 有一个 GIMPLE 树转储窗口,您可以从与“克隆编译器”相同的下拉列表中添加。
clang 用 adc
编译它的事实证明它是合法的,即您想要的 asm 与 C++ 源代码匹配,并且您没有错过一些阻止编译器进行优化的特殊情况。 (假设 clang 没有错误,这里就是这种情况。)
如果你不小心,这个问题肯定会发生,例如试图写一个通用案例 adc
从 3 输入加法中获取进位并提供进位的函数在 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 = -1
的特殊情况下需要忽略进位和 carry_in = 1
.
所以另一个猜测是 gcc 考虑过做 + X
早些时候,因为那个特殊的情况,自己在脚上开枪了。不过,这没有多大意义。
What's the point of using it since it's up to me to provide the carry flag?
您正在使用
_addcarry_u32
正确。它存在的意义在于让你用进位和进位来表达加法,这在纯 C 中很难。GCC 和 clang 没有很好地优化它,通常不只是将进位结果保留在 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]);
}
( On Godbolt with GCC/clang/ICC )
与
unsigned __int128
相比,这是非常低效的编译器只使用 64 位 add/adc,但确实让 clang 和 ICC 发出一串 add
/adc
/adc
/adc
. GCC 搞得一团糟,使用 setcc
将某些步骤的 CF 存储为整数,然后 add dl, -1
将其放回 CF 以获得 adc
.不幸的是,GCC 在用纯 C 编写的扩展精度/大整数方面很糟糕。Clang 有时会稍微好一些,但大多数编译器都不擅长。这就是为什么对于大多数体系结构,最低级别的 gmplib 函数都是用 asm 手写的。
脚注 1 :或对于 uop 计数:在 Intel Haswell 和更早版本上相等,其中
adc
是 2 uop,除了在 Sandybridge-family 的解码器特殊情况下为 1 uop 的立即数为零。但是带有
base + index + disp
的三组分 LEA使它成为 Intel CPU 上的 3 周期延迟指令,所以它肯定更糟。在 Intel Broadwell 及更高版本上,
adc
是一个 1-uop 指令,即使是非零立即数,利用 Haswell 为 FMA 引入的对 3-输入 uop 的支持。所以相等的总 uop 计数但更糟糕的延迟意味着
adc
还是会是更好的选择。https://agner.org/optimize/
关于c++ - 关于 ADC,-1 (0xFFFFFFFF) 有什么特别之处吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56101507/