我遇到了一个问题,g++ 优化了一些它不应该有的东西。我将问题简化为以下示例: 我有一个带有函数
bool my_magic_function(int* x)
的静态库,如果可以的话,它将 x
减 1,否则(x == INT_MIN
),它返回 false
并且不触及原始值。
如果我在调试版本中使用该函数,那么它会按预期工作。但在发布版本中,检查被优化掉了。平台:
在 RHEL 9.3 上使用 g++ (GCC) 11.4.1 20230605 -> 存在问题
Ubuntu 22.04 g++ 11.4.0 g++ 或 10.5.0 g++ -> 存在问题
Ubuntu 22.04 g++ 9.5.0 -> 代码在发布时也按预期工作。
这是一个带有静态库和使用该函数的简单 main.cpp 的最小示例:
almalib.h:
bool my_magic_function(int* x);
almalib.cc:
#include "almalib.h"
#include <cstring>
#include <limits>
bool my_magic_function(int* x) {
int cp_new;
// integer overflow is undefined, so lets make sure it becomes int max
if (*x == std::numeric_limits<int>::lowest()) {
cp_new = std::numeric_limits<int>::max();
} else {
cp_new = *x - 1;
}
if (cp_new < *x) {
*x = cp_new;
return true;
}
return false;
}
主.cpp
#include "almalib.h"
#include <iostream>
#include <limits>
int main()
{
for (int x : {0, std::numeric_limits<int>::lowest()})
{
int x2 = x;
std::cerr << "Res for " << x << " " << (my_magic_function(&x2) ? "OK" : "NOT_OK") << " val: " << x2 << std::endl;
}
}
编译:
g++ -c almalib.cc -o almalib.o
ar crf libalma.a almalib.o
g++ main.cpp -o run -L. -lalma
g++ -c almalib.cc -O3 -o almalibR.o
ar crf libalmaR.a almalibR.o
g++ main.cpp -O3 -o runR -L. -lalmaR
调试输出(./run):
Res for 0 OK val: -1
Res for -2147483648 NOT_OK val: -2147483648
发布的输出(./runR):
Res for 0 OK val: -1
Res for -2147483648 OK val: 2147483647
用gdb检查生成的程序集,
my_magic_function
减少到3行:
0x401320 <_Z17my_magic_functionPi> subl $0x1,(%rdi)
0x401323 <_Z17my_magic_functionPi+3> mov $0x1,%eax
0x401328 <_Z17my_magic_functionPi+8> ret
我的问题是:
这些可能很昂贵,但是
-fwrapv
和 -ftrapv
都能让您的问题消失。
-fwrapv
意味着编译器假定有符号整数的行为与无符号整数类似并且环绕。您的硬件几乎肯定会这样做。 -ftrapv
意味着它会在有符号整数环绕时添加陷阱(异常)(您可能可以在硬件上设置标志来实现这种情况,如果没有,它会添加逻辑来捕获它)。
使用任一标志,您的代码都会正确运行。
虽然
-fwrapv
看起来无害,但这意味着无法完成循环和比较中的一堆优化。
如果没有
-fwrapv
,编译器可以假定同时大于 0 的 a+b
大于 a
且大于 b
。有了它,就不能了。
据猜测,您的编译器首先采用早期分支代码
if (*x == std::numeric_limits<int>::lowest()) {
cp_new = std::numeric_limits<int>::max();
} else {
cp_new = *x - 1;
}
并说“在硬件目标上,这相当于”
cp_new = *x - 1;
因为它知道硬件目标已签署了回绕的下溢。重大优化,消除不必要的分支!
然后它会看
if (cp_new < *x) {
*x = cp_new;
return true;
}
然后替换 cp_new:
if ((*x - 1)< *x) {
*x = (*x - 1);
return true;
}
原因是“嗯,有符号下溢是未定义的行为,所以负 1 总是小于某个值”。从而优化为:
*x = *x-1;
return true;
错误在于它在下溢是
定义并且首先环绕的上下文中使用了
cp_new = *x - 1
,然后在不允许环绕的情况下重新使用它。
通过使下溢导致陷阱或使其假设为真,我们阻止了让它进行第二次错误优化的假设。
但是这个故事 - 为什么
fwrapv
/ftrapv
有效 - 是一个“就是这样的故事”,它并不是通过实际阅读 gcc 代码或错误报告来了解的。