关于ADC的-1(0xFFFFFFFF)有什么特别之处吗?
Is there anything special about -1 (0xFFFFFFFF) regarding ADC?
在我的一个研究项目中,我正在编写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
在那里做什么?阅读携带标志并恢复它的两条指令?真?我希望我错了!
mov
+adc $-1, %eax
在大多数 CPU 上的延迟和 uop 计数方面都比xor
-0 +setc
+ 3 分量lea
更有效,在任何仍然相关的 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
,并因为那个特殊情况而搬起石头砸自己的脚。 不过,这没有多大意义。
使用它有什么意义,因为由我来提供携带标志?
您正确使用_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 计数:在英特尔 Haswell 和更早版本上相等,其中adc
为 2 uops,但 Sandybridge-Family 的解码器特例为零的直接情况为 1 uop。
但是带有base + index + disp
的 3 分量 LEA 使其成为英特尔 CPU 上的 3 周期延迟指令,因此它肯定更糟。
在英特尔 Broadwell 及更高版本上,即使具有非零即时指令,adc
也是 1-uop 指令,利用了 Haswell 为 FMA 引入的对 3 输入 uop 的支持。
因此,相同的总 uop 计数但更差的延迟意味着adc
仍然是更好的选择。
https://agner.org/optimize/
- 为不同配置设置MSVC_RUNTIME_LIBRARY的正确方法是什么
- 警告处理为错误这里有什么问题
- 什么时候调用组成单元对象的析构函数
- #定义c-预处理器常量..我做错了什么
- 努力将整数转换为链表。不知道我在这里做错了什么
- C++我的数学有什么问题,为什么我的代码不能正确循环
- 什么时候在C++中返回常量引用是个好主意
- 当在同一名称空间中有两个具有相同签名的函数时,会发生什么
- C++避免重复声明的语法是什么
- c++库的公共头文件中应该包含什么
- 问题:什么是QAbstractItemView::NoEditTriggers的反面
- 有什么方法可以遍历结构吗
- 当类在C++中定义时,有什么方法可以"register"类吗?
- ifstream什么都没读
- 在C++中,将大的无符号浮点数四舍五入为整数的最佳方法是什么
- 实现无开销push_back的最佳方法是什么
- C++从另一个类访问公共静态向量的正确方法是什么
- "throw expression code" 1e7 >返回 d 是什么?投掷标准::overflow_error( "too big" ) : d;意味 着?
- 我应该使用什么来代替void作为变体中的替代类型之一
- 关于ADC的-1(0xFFFFFFFF)有什么特别之处吗?