再次不稳定:有必要阻止优化吗?
Once more volatile: necessary to prevent optimization?
我已经阅读了很多关于 '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' 的值为真。
现在我猜允许编译器将 while(working) 优化为 while(true) 因为 'working' 的值在开始循环时为真。
- 如果不是这种情况,那就意味着这样的事情效率很低:
for(int i = 0; i < someOtherClassMember; i++)
{
doSomething();
}
...因为每次迭代都必须加载 someOtherClassMember 的值。
- 如果 是 这种情况,我认为 'working' 必须是 volatile 以防止编译器优化它。
这两个是哪个?在谷歌搜索 volatile 的使用时,我发现人们声称它仅在使用 I/O 设备直接写入内存时有用,但我也发现它应该在某个场景中使用像我一样。
Volatile
将使 while 循环在每次检查时重新加载 working
变量。实际上,这通常允许您通过从异步信号处理程序或另一个线程调用 stopWorking
来停止工作函数,但根据标准,这还不够。该标准需要无锁原子或volatile sig_atomic_t
类型的变量用于sighandler <->常规上下文通信和原子用于线程间通讯.
您的程序将优化为无限循环†。
void foo() { A{}.work(); }
编译为 (g++ with O2)
foo():
sub rsp, 8
.L2:
call processSomeJob()
jmp .L2
该标准定义了假设的抽象机器 将如何处理程序。符合标准的编译器必须编译您的程序,使其在所有可观察行为中都以与该机器相同的方式运行。这被称为 as-if 规则,只要 what 你的程序所做的是相同的,编译器就有自由,而不管 如何.
通常情况下,读取和写入变量并不构成可观察的,这就是编译器可以根据需要省略尽可能多的读取和写入的原因。编译器可以看到 working
没有被分配并优化读取。 volatile
的(经常被误解的)效果恰恰是让它们可观察,这迫使编译器单独留下读取和写入‡.
但是等你说,另一个线程可能会分配给 working
。这就是未定义行为的余地所在。当存在未定义行为时,编译器可能会做 任何事情,包括格式化硬盘驱动器并且仍然符合标准。由于没有同步并且 working
不是原子的,因此写入 working
的任何其他线程都是数据竞争,这是无条件未定义的行为。因此,无限循环唯一错误的情况是存在未定义的行为,编译器由此决定您的程序还不如继续循环。
TL;DR 不要对多线程使用纯 bool
和 volatile
。使用 std::atomic<bool>
.
†并非在所有情况下。 void bar(A& a) { a.work(); }
不适用于某些版本。
‡实际上,这里有一些。
Now I'm guessing the compiler is allowed to optimize the while(working) to while(true)
可能,是的。但前提是它可以证明 processSomeJob()
不会修改 working
变量,即如果它可以证明循环是无限的。
If this is not the case, that would mean something like this would be quite inefficient ... as the value of someOtherClassMember would have to be loaded each iteration
你的推理很有道理。但是,内存位置可能保留在缓存中,并且从 CPU 缓存中读取不一定会非常慢。如果 doSomething
复杂到足以导致 someOtherClassMember
从缓存中被驱逐,那么我们肯定必须从内存中加载,但另一方面 doSomething
可能非常复杂以至于相比之下,单个内存负载是微不足道的。
Which of these two is the case?
要么。优化器将无法分析所有可能的代码路径;我们不能假设在所有情况下都可以优化循环。但是如果someOtherClassMember
在任何代码路径中都被证明没有被修改,那么证明它在理论上是可能的,因此循环在理论上是可以优化的。
but I also find claims that [volatile] should be used in a scenario like mine.
volatile
在这里帮不了你。如果 working
在另一个线程中被修改,则存在数据竞争。而数据竞争意味着程序的行为是未定义的。
为避免数据竞争,您需要同步:使用互斥锁或原子操作来跨线程共享访问。
我已经阅读了很多关于 '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' 的值为真。
现在我猜允许编译器将 while(working) 优化为 while(true) 因为 'working' 的值在开始循环时为真。
- 如果不是这种情况,那就意味着这样的事情效率很低:
for(int i = 0; i < someOtherClassMember; i++) { doSomething(); }
...因为每次迭代都必须加载 someOtherClassMember 的值。
- 如果 是 这种情况,我认为 'working' 必须是 volatile 以防止编译器优化它。
这两个是哪个?在谷歌搜索 volatile 的使用时,我发现人们声称它仅在使用 I/O 设备直接写入内存时有用,但我也发现它应该在某个场景中使用像我一样。
Volatile
将使 while 循环在每次检查时重新加载 working
变量。实际上,这通常允许您通过从异步信号处理程序或另一个线程调用 stopWorking
来停止工作函数,但根据标准,这还不够。该标准需要无锁原子或volatile sig_atomic_t
类型的变量用于sighandler <->常规上下文通信和原子用于线程间通讯.
您的程序将优化为无限循环†。
void foo() { A{}.work(); }
编译为 (g++ with O2)
foo():
sub rsp, 8
.L2:
call processSomeJob()
jmp .L2
该标准定义了假设的抽象机器 将如何处理程序。符合标准的编译器必须编译您的程序,使其在所有可观察行为中都以与该机器相同的方式运行。这被称为 as-if 规则,只要 what 你的程序所做的是相同的,编译器就有自由,而不管 如何.
通常情况下,读取和写入变量并不构成可观察的,这就是编译器可以根据需要省略尽可能多的读取和写入的原因。编译器可以看到 working
没有被分配并优化读取。 volatile
的(经常被误解的)效果恰恰是让它们可观察,这迫使编译器单独留下读取和写入‡.
但是等你说,另一个线程可能会分配给 working
。这就是未定义行为的余地所在。当存在未定义行为时,编译器可能会做 任何事情,包括格式化硬盘驱动器并且仍然符合标准。由于没有同步并且 working
不是原子的,因此写入 working
的任何其他线程都是数据竞争,这是无条件未定义的行为。因此,无限循环唯一错误的情况是存在未定义的行为,编译器由此决定您的程序还不如继续循环。
TL;DR 不要对多线程使用纯 bool
和 volatile
。使用 std::atomic<bool>
.
†并非在所有情况下。 void bar(A& a) { a.work(); }
不适用于某些版本。
‡实际上,这里有一些
Now I'm guessing the compiler is allowed to optimize the while(working) to while(true)
可能,是的。但前提是它可以证明 processSomeJob()
不会修改 working
变量,即如果它可以证明循环是无限的。
If this is not the case, that would mean something like this would be quite inefficient ... as the value of someOtherClassMember would have to be loaded each iteration
你的推理很有道理。但是,内存位置可能保留在缓存中,并且从 CPU 缓存中读取不一定会非常慢。如果 doSomething
复杂到足以导致 someOtherClassMember
从缓存中被驱逐,那么我们肯定必须从内存中加载,但另一方面 doSomething
可能非常复杂以至于相比之下,单个内存负载是微不足道的。
Which of these two is the case?
要么。优化器将无法分析所有可能的代码路径;我们不能假设在所有情况下都可以优化循环。但是如果someOtherClassMember
在任何代码路径中都被证明没有被修改,那么证明它在理论上是可能的,因此循环在理论上是可以优化的。
but I also find claims that [volatile] should be used in a scenario like mine.
volatile
在这里帮不了你。如果 working
在另一个线程中被修改,则存在数据竞争。而数据竞争意味着程序的行为是未定义的。
为避免数据竞争,您需要同步:使用互斥锁或原子操作来跨线程共享访问。