常量 folding/propagation 内存屏障优化

Constant folding/propagation optimization with memory barriers

我已经阅读了一段时间,以便更好地理解使用现代(多核)CPU 进行多线程编程时发生了什么。然而,当我阅读 this 时,我注意到下面 "Explicit Compiler Barriers" 部分的代码,它没有为 IsPublished 全局使用 volatile。

#define COMPILER_BARRIER() asm volatile("" ::: "memory")

int Value;
int IsPublished = 0;

void sendValue(int x)
{
    Value = x;
    COMPILER_BARRIER();          // prevent reordering of stores
    IsPublished = 1;
}

int tryRecvValue()
{
    if (IsPublished)
    {
        COMPILER_BARRIER();      // prevent reordering of loads
        return Value;
    }
    return -1;  // or some other value to mean not yet received
}

问题是,在这里为 IsPublished 省略 volatile 是否安全?许多人提到 "volatile" 关键字与多线程编程没有太大关系,我同意他们的看法。但是,在编译器优化期间,可以应用 "Constant Folding/Propagation",并且如 wiki page 所示,如果编译器不太了解谁可以更改,则可以将 if (IsPublished) 更改为 if (false) IsPublished 的值。我在这里错过或误解了什么吗?

内存屏障可以防止 CPU 的编译器排序和乱序执行,但正如我在前面的段落中所说,我仍然需要 volatile 以避免 "Constant Folding/Propagation" 这是一个危险的优化,尤其是在无锁代码中使用全局变量作为标志?

如果 tryRecvValue() 被调用一次,可以安全地为 IsPublished 省略 volatile。如果在对 tryRecvValue() 的调用之间有一个函数调用,编译器无法证明它不会更改 falseIsPublished 值,情况也是如此].

// Example 1(Safe)
int v = tryRecvValue();
if(v == -1) exit(1);

// Example 2(Unsafe): tryRecvValue may be inlined and 'IsPublished' may be not re-read between iterations.
int v;
while(true)
{
    v = tryRecvValue();
    if(v != -1) break;
}

// Example 3(Safe)
int v;
while(true)
{
    v = tryRecvValue();
    if(v != -1) break;
    some_extern_call(); // Possibly can change 'IsPublished'
}

只有当编译器可以证明变量的值时,才能应用常量传播。因为 IsPublished 被声明为非常量,所以只有在以下情况下才能证明其值:

  1. 变量赋给给定值从变量读取后跟分支,仅在变量已给定值时执行。
  2. 同一程序的线程中(再次)读取变量。

  3. 2 和 3 之间的变量未更改在给定程序的线程中

除非您在某种 .init 函数中调用 tryRecvValue(),否则编译器永远不会在读取的同一个线程中看到 IsPublished 初始化。因此,无法根据其初始化证明此变量的 false 值。

根据 false(空)分支证明 IsPublishedfalsetryRecvValue函数是可以的,见上面代码中的Example 2