常量 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()
的调用之间有一个函数调用,编译器无法证明它不会更改 false 的 IsPublished
值,情况也是如此].
// 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
被声明为非常量,所以只有在以下情况下才能证明其值:
- 变量赋给给定值或从变量读取后跟分支,仅在变量已给定值时执行。
在同一程序的线程中(再次)读取变量。
2 和 3 之间的变量未更改在给定程序的线程中。
除非您在某种 .init 函数中调用 tryRecvValue()
,否则编译器永远不会在读取的同一个线程中看到 IsPublished 初始化。因此,无法根据其初始化证明此变量的 false 值。
根据 false(空)分支证明 IsPublished 的 false 值 tryRecvValue
函数是可以的,见上面代码中的Example 2
。
我已经阅读了一段时间,以便更好地理解使用现代(多核)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()
的调用之间有一个函数调用,编译器无法证明它不会更改 false 的 IsPublished
值,情况也是如此].
// 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
被声明为非常量,所以只有在以下情况下才能证明其值:
- 变量赋给给定值或从变量读取后跟分支,仅在变量已给定值时执行。
在同一程序的线程中(再次)读取变量。
2 和 3 之间的变量未更改在给定程序的线程中。
除非您在某种 .init 函数中调用 tryRecvValue()
,否则编译器永远不会在读取的同一个线程中看到 IsPublished 初始化。因此,无法根据其初始化证明此变量的 false 值。
根据 false(空)分支证明 IsPublished 的 false 值 tryRecvValue
函数是可以的,见上面代码中的Example 2
。