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

标签 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 会相应地改变)。不过我发现了一个异常(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, %eaxxor更有效率-零 + 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/

相关文章:

c++ - std::bind 到 void* 到 std::function

c++ - 错误 : `boost' has not been declared

assembly - 二进制到BCD转换

c - 在 C (GNU/Linux) 中运行动态生成的程序集

c++ - Qt Windows 部署 : Application does not start

c++ - Windows 上的 Cmake 不添加共享库路径(适用于 linux)

gcc - 对 YAML::Load 的 undefined reference

c# - 来自 C# : "DLL initialization routine failed" on Windows 10 的 gcc DLL

c - 使用 RPATH 但不使用 RUNPATH?

linux - Fork 系统调用失败后 rax 中的返回值是多少?