再次波动:必须防止优化?

问题描述 投票:3回答:3

我一直在阅读关于'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(工作)到while(true),因为'working'的值在启动循环时为true。 如果不是这种情况,那就意味着这样的效率会非常低效: for(int i = 0; i < someOtherClassMember; i++) { doSomething(); } ...因为someOtherClassMember的值必须在每次迭代时加载。 如果是这种情况,我认为“工作”必须是易失性的,以防止编译器对其进行优化。

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

c++ memory volatile
3个回答
2
投票

您的程序将被优化为无限循环†。

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

被编译为(g ++与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(); }不适用于某些版本。 ‡实际上,这周围有一些debate


2
投票

现在我猜测编译器可以优化while(工作)到while(true)

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

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

你的推理是合理的。但是,内存位置可能保留在缓存中,从CPU缓存读取不一定非常慢。如果doSomething复杂到足以导致someOtherClassMember从缓存中被逐出,那么肯定我们必须从内存加载,但另一方面doSomething可能是如此复杂以至于单个内存负载相比之下微不足道。

这两个中哪一个是这样的?

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

但我也发现声称应该在像我这样的场景中使用[volatile]。

volatile在这里没有帮助你。如果在另一个线程中修改了working,那么就会出现数据竞争。数据竞争意味着程序的行为是不确定的。

要避免数据竞争,您需要同步:使用互斥或​​原子操作来共享跨线程的访问。


1
投票

Volatile将使while循环在每次检查时重新加载working变量。实际上,通常允许您通过调用从异步信号处理程序或另一个线程调用的stopWorking来停止工作函数,但根据标准,这还不够。该标准要求无锁原子或volatile sig_atomic_t类型的变量用于sighandler常规上下文通信和用于线程间通信的原子。

© www.soinside.com 2019 - 2024. All rights reserved.