为何编译器将条件循环变成无限循环?

Visual C ++编译器的用户之一给出了以下代码示例,并询问了为什么循环循环执行该条件,尽管在某些时候条件应该停止并且循环应该结束:

#include <windows.h> int x = 0, y = 1; int* ptr; DWORD CALLBACK ThreadProc(void*) { Sleep(1000); ptr = &y; return 0; } int main(int, char**) { ptr = &x; // starts out pointing to x DWORD id; HANDLE hThread = CreateThread(nullptr, 0, ThreadProc, 0, &id); // ,        ptr //     while (*ptr == 0) { } return 0; } 

对于不熟悉Windows平台特定功能的用户,以下是纯C ++中的等效功能:

 #include <chrono> #include <thread> int x = 0, y = 1; int* ptr = &x; void ThreadProc() { std::this_thread::sleep_for(std::chrono::seconds(1)); ptr = &y; } int main(int, char**) { ptr = &x; // starts out pointing to x std::thread thread(ThreadProc); // ,        ptr //     while (*ptr == 0) { } return 0; } 

接下来,用户带来了对该程序的理解:
编译器已将条件循环变为无限循环。 我从生成的汇编代码中看到了这一点,该代码一旦将ptr指针的值加载到寄存器中(在循环开始时),然后在每次迭代时将该寄存器的值与零进行比较。 由于从ptr重新加载值不会再发生,因此循环永远不会结束。

我知道将ptr声明为“ volatile int *”会导致编译器放弃优化并在每次循环迭代时读取ptr值,这将解决此问题。 但是我仍然想知道为什么编译器不能足够聪明地自动执行这些操作? 显然,可以更改两个不同线程中使用的全局变量,这意味着不能简单地将其缓存在寄存器中。 为什么编译器不能立即生成正确的代码?


在回答这个问题之前,让我们从一些挑剔的事情开始:“ volatile int * ptr”没有将ptr变量声明为“禁止优化的指针”。 这是“禁止优化的变量的正常指针”。 上述问题的作者想到的就是声明为“ int * volatile ptr”。

现在回到主要问题。 这是怎么回事

即使只是粗略地浏览一下代码,也会告诉我们没有像std :: atomic这样的变量,也没有使用std :: memory_order(显式或隐式)。 这意味着从两个不同的流访问ptr或* ptr的任何尝试都将导致未定义的行为。 直观地,您可以这样想:“编译器优化每个线程,就像在程序中单独运行一样。 编译器必须考虑从不同流访问数据的唯一点是使用std :: atomic或std :: memory_order。”

这就解释了为什么程序无法像程序员期望的那样工作。 从您允许模糊行为的那一刻起-绝对不能保证。

但是好吧,让我们考虑一下他的问题的第二部分:为什么编译器不够聪明,无法识别这种情况,而是通过将指针值加载到寄存器中来自动关闭优化? 好了,编译器会自动应用所有可能的方法,并且不会违反优化标准。 要求他能够阅读程序员的思想并关闭一些与标准不矛盾的优化,这很奇怪,也许程序员认为这应该使程序的逻辑更好。 “哦,如果这个循环期望另一个线程中的全局变量的值发生变化,尽管它尚未明确宣布怎么办? 我会花一百倍的速度来减慢速度,为这种情况做准备!” 应该是这样吗? 几乎没有

但是,假设我们向编译器添加了一条规则,例如“如果优化导致出现无限循环,那么您需要取消它并收集未经优化的代码。” 甚至像这样:“成功取消各个优化,直到结果为非无限循环。” 除了这将带来令人惊讶的惊喜之外,它是否还能带来任何好处?

是的,在这种理论情况下,我们不会陷入无限循环。 如果其他一些流向* ptr写一个非零值,它将被中断。 如果另一个线程将非零值写入变量x,也会被中断。 尚不清楚应该进行多大的依赖性分析以“捕获”可能影响情况的所有案例。 由于编译器实际上并未运行所创建的程序,也不在运行时分析其行为,因此,唯一的出路是假设通常没有优化全局变量,指针和引用的调用。

 int limit; void do_something() { ... if (value > limit) value = limit; //   limit ... for (i = 0; i < 10; i++) array[i] = limit; //   limit ... } 

这完全违背了C ++的精神。 语言标准说,如果您修改变量并希望在另一个线程中看到此修改,则应该明确地这样说:使用原子操作或组织对内存的访问(通常使用同步对象)。

所以,请这样做。

Source: https://habr.com/ru/post/zh-CN424729/


All Articles