更加不稳定:是否需要阻止优化?

时间:2019-02-18 09:57:25

标签: c++ memory volatile

我已经阅读了很多有关'volatile'关键字的内容,但是我仍然没有明确的答案。

考虑以下代码:

class A
{
public:
    void work()
    {
        working = true;

        while(working)
        {
            processSomeJob();
        }
    }

    void stopWorking() // Can be called from another thread
    {
        working = false;
    }
private:
    bool working;
}

work()进入其循环时,“ working”的值为true。

  • 现在,我猜测编译器可以将 while(working)优化为 while(true),因为'working'的值为true启动循环时。

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

    ...因为每次迭代都必须加载someOtherClassMember的值。

    • 如果是 情况,我认为“工作”必须是易变的,以防止编译器对其进行优化。

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

3 个答案:

答案 0 :(得分:2)

您的程序优化为无限循环

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

被编译为(带有O2的g ++)

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

该标准定义了假设的抽象机对程序的处理方式。符合标准的编译器必须编译您的程序,使其在所有可观察的行为中均与该计算机具有相同的行为。这就是所谓的 as 规则,只要您的程序执行的 是相同的,无论如何,编译器都有自由。

通常,对变量的读写并不构成可观察性,这就是为什么编译器可以根据需要进行尽可能多的读写操作。编译器可以看到working未被分配,并优化了读取。 volatile的(通常被误解)效果恰恰是使它们可观察,这迫使编译器将读取和写入操作留为单独操作

但是,请您说,另一个线程可能会分配给working。这就是出现未定义行为的余地的地方。当出现未定义行为(包括格式化硬盘并仍然符合标准)时,编译器可能会执行任何操作。由于没有同步并且working不是原子的,因此任何其他写入working的线程都是数据竞争,这是无条件的未定义行为。因此,无限循环唯一的错误是出现未定义的行为,编译器据此决定您的程序也应该继续循环。

TL; DR请勿将普通boolvolatile用于多线程。使用std::atomic<bool>

†​​并非在所有情况下都如此。 void bar(A& a) { a.work(); }不适用于某些版本。
‡实际上,这周围有一些debate

答案 1 :(得分:2)

  

现在我猜是允许编译器将while(working)优化为while(true)

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

  

如果不是这种情况,那将意味着这样的效率将非常低下……因为someOtherClassMember的值必须在每次迭代时都加载

您的推理是正确的。但是,内存位置可能仍保留在缓存中,并且从CPU缓存中读取数据并不一定会显着降低速度。如果doSomething的复杂程度足以导致someOtherClassMember被从缓存中逐出,那么请确保我们必须从内存中进行加载,但是另一方面,doSomething可能是如此复杂,以至于相比而言,单个内存的负载微不足道。

  

这两个是哪个?

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

  

但是我也发现,在我的情况下应该使用[volatile]。

volatile在这里无济于事。如果working在另一个线程中被修改,则存在数据争用。数据竞争意味着程序的行为是不确定的。

为避免数据争用,您需要进行同步:使用互斥锁或原子操作在线程之间共享访问。

答案 2 :(得分:1)

Volatile将使while循环在每次检查时重新加载working变量。实际上,这通常允许您通过从异步信号处理程序或其他线程进行的stopWorking调用来停止工作功能,但按照标准,这还不够。该标准要求无锁原子或类型为volatile sig_atomic_t的变量用于sighandler <->常规上下文通信,以及 atomics 用于线程间通信。