这种编译器优化不一致是否完全由未定义的行为解释?
Is this compiler optimization inconsistency entirely explained by undefined behaviour?
在前几天与几位同事的讨论中,我用 C++ 拼凑了一段代码来说明内存访问冲突。
在长时间几乎完全使用具有垃圾收集功能的语言之后,我目前正在慢慢回归 C++,而且我想,我失去了触摸显示,因为我对我的行为感到非常困惑短节目展出
有问题的代码是这样的:
#include <iostream>
using std::cout;
using std::endl;
struct A
{
int value;
};
void f()
{
A* pa; // Uninitialized pointer
cout<< pa << endl;
pa->value = 42; // Writing via an uninitialized pointer
}
int main(int argc, char** argv)
{
f();
cout<< "Returned to main()" << endl;
return 0;
}
我在 Ubuntu 15.04 上用 GCC 4.9.2 编译了它,并设置了 -O2
编译器标志。当 运行 时,我的期望是当我的注释表示为 "writing via an uninitialized pointer" 的行被执行时它会崩溃。
然而,出乎我意料的是,程序运行成功地结束了,产生了以下输出:
0
Returned to main()
我用 -O0
标志重新编译了代码(以禁用所有优化)并再次 运行 程序。这一次,行为如我所料:
0
Segmentation fault
(好吧,差不多:我没想到一个指针会被初始化为0。)基于这个观察,我推测在用-O2
编译时设置,致命指令被优化掉。这是有道理的,因为在有问题的行设置 pa->value
之后,没有进一步的代码访问它,因此,据推测,编译器确定它的删除不会修改程序的可观察行为。
我重复了几次,每次程序在没有优化的情况下编译时都会崩溃,而在使用 -O2
.
编译时奇迹般地工作
当我添加一行输出 pa->value
到 f()
的主体末尾时,我的假设得到进一步证实:
cout<< pa->value << endl;
正如预期的那样,有了这一行,程序总是崩溃,无论编译它的优化级别如何。
如果到目前为止我的假设是正确的,这一切都是有道理的。
但是,如果我将代码从 f()
的主体直接移动到 main()
,我的理解就会有所不同,就像这样:
int main(int argc, char** argv)
{
A* pa;
cout<< pa << endl;
pa->value = 42;
cout<< pa->value << endl;
return 0;
}
禁用优化后,此程序如预期的那样崩溃。然而,使用 -O2
,程序成功运行到最后并产生以下输出:
0
42
这对我来说毫无意义。
This answer 提到 "dereferencing a pointer that has not yet been definitely initialized",这正是我正在做的,作为 C++ 中未定义行为的来源之一。
那么,与 f()
中的代码相比,优化对 main()
中代码的影响方式是否存在差异,完全可以通过我的程序包含 UB 的事实来解释,因此编译器是"go nuts" 在技术上是免费的,或者与其他例程中的代码相比,main()
中的代码优化方式之间是否存在一些我不知道的根本差异?
您的程序有未定义的行为。这意味着任何事情都可能发生。该程序根本不包含在 C++ 标准中。您不应该抱有任何期望。
人们常说未定义的行为可能 "launch missiles" 或 "cause demons to fly out of your nose",以强化这一点。后者比较牵强但是前者是可行的,想象一下你的代码在一个核发射场上,野指针恰好写了一段启动global thermouclear的内存war..
写入未知指针一直是可能产生未知后果的事情。更糟糕的是当前流行的哲学,它建议编译器应该假设程序永远不会接收导致 UB 的输入,因此应该优化任何代码来测试这些输入,如果这样的测试不会阻止 UB 的发生。
因此,例如,给定:
uint32_t hey(uint16_t x, uint16_t y)
{
if (x < 60000)
launch_missiles();
else
return x*y;
}
void wow(uint16_t x)
{
return hey(x,40000);
}
32 位编译器可以合法地用无条件调用替换 wow
launch_missiles
不考虑 x
的值,因为 x
"can't possibly" 大于 53687(任何超出该值的值都会导致 x*y 的计算溢出。即使C89 的作者指出,那个时代的大多数编译器在上述情况下都会计算出正确的结果,因为标准没有对编译器施加任何要求,超现代哲学将其视为 "more efficient"编译器假设程序永远不会收到需要依赖此类东西的输入。
在前几天与几位同事的讨论中,我用 C++ 拼凑了一段代码来说明内存访问冲突。
在长时间几乎完全使用具有垃圾收集功能的语言之后,我目前正在慢慢回归 C++,而且我想,我失去了触摸显示,因为我对我的行为感到非常困惑短节目展出
有问题的代码是这样的:
#include <iostream>
using std::cout;
using std::endl;
struct A
{
int value;
};
void f()
{
A* pa; // Uninitialized pointer
cout<< pa << endl;
pa->value = 42; // Writing via an uninitialized pointer
}
int main(int argc, char** argv)
{
f();
cout<< "Returned to main()" << endl;
return 0;
}
我在 Ubuntu 15.04 上用 GCC 4.9.2 编译了它,并设置了 -O2
编译器标志。当 运行 时,我的期望是当我的注释表示为 "writing via an uninitialized pointer" 的行被执行时它会崩溃。
然而,出乎我意料的是,程序运行成功地结束了,产生了以下输出:
0
Returned to main()
我用 -O0
标志重新编译了代码(以禁用所有优化)并再次 运行 程序。这一次,行为如我所料:
0
Segmentation fault
(好吧,差不多:我没想到一个指针会被初始化为0。)基于这个观察,我推测在用-O2
编译时设置,致命指令被优化掉。这是有道理的,因为在有问题的行设置 pa->value
之后,没有进一步的代码访问它,因此,据推测,编译器确定它的删除不会修改程序的可观察行为。
我重复了几次,每次程序在没有优化的情况下编译时都会崩溃,而在使用 -O2
.
当我添加一行输出 pa->value
到 f()
的主体末尾时,我的假设得到进一步证实:
cout<< pa->value << endl;
正如预期的那样,有了这一行,程序总是崩溃,无论编译它的优化级别如何。
如果到目前为止我的假设是正确的,这一切都是有道理的。
但是,如果我将代码从 f()
的主体直接移动到 main()
,我的理解就会有所不同,就像这样:
int main(int argc, char** argv)
{
A* pa;
cout<< pa << endl;
pa->value = 42;
cout<< pa->value << endl;
return 0;
}
禁用优化后,此程序如预期的那样崩溃。然而,使用 -O2
,程序成功运行到最后并产生以下输出:
0
42
这对我来说毫无意义。
This answer 提到 "dereferencing a pointer that has not yet been definitely initialized",这正是我正在做的,作为 C++ 中未定义行为的来源之一。
那么,与 f()
中的代码相比,优化对 main()
中代码的影响方式是否存在差异,完全可以通过我的程序包含 UB 的事实来解释,因此编译器是"go nuts" 在技术上是免费的,或者与其他例程中的代码相比,main()
中的代码优化方式之间是否存在一些我不知道的根本差异?
您的程序有未定义的行为。这意味着任何事情都可能发生。该程序根本不包含在 C++ 标准中。您不应该抱有任何期望。
人们常说未定义的行为可能 "launch missiles" 或 "cause demons to fly out of your nose",以强化这一点。后者比较牵强但是前者是可行的,想象一下你的代码在一个核发射场上,野指针恰好写了一段启动global thermouclear的内存war..
写入未知指针一直是可能产生未知后果的事情。更糟糕的是当前流行的哲学,它建议编译器应该假设程序永远不会接收导致 UB 的输入,因此应该优化任何代码来测试这些输入,如果这样的测试不会阻止 UB 的发生。
因此,例如,给定:
uint32_t hey(uint16_t x, uint16_t y)
{
if (x < 60000)
launch_missiles();
else
return x*y;
}
void wow(uint16_t x)
{
return hey(x,40000);
}
32 位编译器可以合法地用无条件调用替换 wow
launch_missiles
不考虑 x
的值,因为 x
"can't possibly" 大于 53687(任何超出该值的值都会导致 x*y 的计算溢出。即使C89 的作者指出,那个时代的大多数编译器在上述情况下都会计算出正确的结果,因为标准没有对编译器施加任何要求,超现代哲学将其视为 "more efficient"编译器假设程序永远不会收到需要依赖此类东西的输入。