再次波动:防止优化所必需

Once more volatile: necessary to prevent optimization?

本文关键字:优化      更新时间:2023-10-16

我已经阅读了很多有关'挥发性'关键字的信息,但我仍然没有确定的答案。

考虑此代码:

class A
{
public:
    void work()
    {
        working = true;
        while(working)
        {
            processSomeJob();
        }
    }
    void stopWorking() // Can be called from another thread
    {
        working = false;
    }
private:
    bool working;
}

当工作()进入其循环时,"工作"的价值是正确的。

  • 现在,我猜允许编译器在(工作) to 时优化,而(true)是'working'的值开始循环时。

    • 如果不是这种情况,那将意味着这样的事情效率很低:
    for(int i = 0; i < someOtherClassMember; i++)
    {
        doSomething(); 
    }
    

    ...由于必须加载某些hotherclassmember的值。

    • 如果此,我认为"工作"必须是,以防止编译器优化。

这两个情况中的哪一个?当搜索使用 domatile 的使用时,我发现人们声称它只有在使用I/O设备直接写入内存时才有用,但是我也发现应该在像我这样的场景中使用它。

您的程序 Will 被优化到无限环中。

void foo() { A{}.work(); }

被编译为(使用O2)

foo():
        sub     rsp, 8
.L2:
        call    processSomeJob()
        jmp     .L2

该标准定义了假设的抽象机器对程序有什么作用。符合标准的编译器必须编译您的程序,以与所有可观察的行为中的机器相同。这被称为 as-if 规则,编译器具有自由度,只要您的程序做什么,无论如何如何。

通常,读取和写入变量并不构成可观察到的,这就是为什么编译器可以尽可能多地读取和写作的原因。编译器可以看到working不会分配给并优化读书。volatile的(通常被误解)效果完全是为了使它们可观察到,这迫使编译器留下读取并单独写入

但是,等等,另一个线程可能分配给working。这是不确定行为的余地所在的地方。编译器可能会在不确定的行为时做任何事情,包括格式化硬盘驱动器并仍然是标准符合的。由于没有同步,并且working不是原子化,因此任何其他写入working的线程都是数据竞赛,这是无条件未定义的行为。因此,无限循环是错误的唯一一次是在不确定的行为时,编译器决定您的程序不妨继续进行循环。

tl; dr不要使用普通的boolvolatile进行多线程。使用std::atomic<bool>

†并非在所有情况下。void bar(A& a) { a.work(); }不适合某些版本。
‡实际上,围绕此问题进行了一些争论。

现在,我猜允许编译器优化(工作)while(true)

可能,是的。但是,只有它可以证明processSomeJob()不会修改working变量,即是否可以证明循环是无限的。

如果不是这种情况,那将意味着这样的事情效率很低……因为必须加载某些hotherclassmember的值

您的推理是正确的。但是,内存位置可能保留在缓存中,并且从CPU缓存中读取不一定会显着慢。如果doSomething足够复杂,可以导致someOtherClassMember从缓存中驱逐,则确保我们必须从内存加载,但另一方面,doSomething可能是如此复杂,以至于单个内存负载相比无关紧要。

这两种情况中的哪一个?

。优化器将无法分析所有可能的代码路径;我们不能假设在所有情况下都可以优化循环。但是,如果证明someOtherClassMember在任何代码路径中都没有修改,那么从理论上证明这是可能的,因此可以在理论上优化循环。

,但我也发现说[挥发性]应该在我的场景中使用。

volatile在这里对您没有帮助。如果在另一个线程中修改了working,则有一个数据竞赛。数据竞赛意味着程序的行为是未定义的。

要避免进行数据竞赛,您需要同步:使用MUTEX或原子操作来共享跨线程的访问。

Volatile将在每次检查中重新加载working变量。实际上,这通常会允许您使用由异步信号处理程序或其他线程制成的stopWorking的调用来停止工作功能,但是根据标准,它不够。该标准需要或Sighandler&lt; ->常规上下文通信的volatile sig_atomic_t的变量和 Anomics 用于线程间的通信。