为什么编译器允许优化掉这个忙等待循环?

8
#include <iostream>
#include <thread>
#include <mutex>

int main()
{
    std::atomic<bool> ready = false;

    std::thread threadB = std::thread([&]() {
        while (!ready) {}

        printf("Hello from B\n");
    });

    std::this_thread::sleep_for(std::chrono::seconds(1));

    printf("Hello from A\n");

    ready = true;

    threadB.join();

    printf("Hello again from A\n");
}

这是来自CppCon演讲https://www.youtube.com/watch?v=F6Ipn7gCOsY&ab_channel=CppCon的一个例子(17分钟)。

目标是先打印Hello from A,然后允许threadB启动。很明显,应该避免繁忙等待,因为它会使用大量CPU。

作者说while (!ready) {}循环可以被编译器优化(通过将ready的值放入寄存器),因为编译器看到threadB从不休眠,所以ready永远不会改变。但即使线程从不休眠,其他线程仍然可以更改该值,对吗?由于ready是原子的,因此不存在数据竞争。作者指出这段代码是UB。有人能解释一下为什么编译器允许这样的优化吗?


5
我认为演讲者是错误的,这个循环不能被优化掉。std::atomic 的整个意义在于它实际上可以突然改变,编译器不能做其他假设。去除这个循环将非常改变程序的可观察行为,因此不是一个有效的优化。 - Igor Tandetnik
Sleep告诉CPU在一定时间内不需要工作,这就是它的全部作用(与逻辑占用相同时间没有区别),除非循环为空,否则循环不会被优化掉(删除)。 - Top-Master
2
@Top-Master 循环不是空的 - 它调用了ready.load() - Igor Tandetnik
我认为由于这里有两个选择(优化掉循环,不优化掉循环),所以这将是一个关于是否为未指定行为的问题,而不是未定义行为。 - Gonen I
最好将原子操作视为防止切片和排序。它们并不隐式具有易变性。 - doug
1个回答

10

在视频下方的评论中,作者承认自己错了:

我一开始也这么想,但是现在看来我错了;编译器无法将原子读取提升到循环外。@17:54处的建议仍然正确 - 你应该非常小心,并注意编译器可能会重新排序、合并或消除原子访问的情况 - 但是这个while循环实际上并不是这样的情况。 关于编译器如何优化原子访问模式的一些(主要是理论上的)例子,请参见JF Bastien的N4455“没有理智的编译器会优化原子操作” http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4455.html


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接