编译器可以从全局变量读取两次,而不是存储本地变量吗?

Gan*_*rei 3 c c++ multithreading compiler-optimization

我最近一直试图重新熟悉多线程,并发现了这篇论文.其中一个例子说使用这样的代码时要小心:

int my_counter = counter; // Read global
int (* my_func) (int);
if (my_counter > my_old_counter) {
  ... // Consume data
  my_func = ...;
  ... // Do some more consumer work
}
... // Do some other work
if (my_counter > my_old_counter) {
... my_func(...) ...
}
Run Code Online (Sandbox Code Playgroud)

说明:

如果编译器决定它需要在两个测试之间溢出包含我的计数器的寄存器,它可能决定避免存储该值(毕竟它只是一个计数器的副本),而是简单地重新读取值计数器进行第二次比较涉及我的柜台[...]

这样做可以将代码转换为:

int my_counter = counter; // Read global
int (* my_func) (int);
if (my_counter > my_old_counter) {
  ... // Consume data
  my_func = ...;
  ... // Do some more consumer work
}
... // Do some other work
my_counter = counter; // Reread global!
if (my_counter > my_old_counter) {
... my_func(...) ...
}
Run Code Online (Sandbox Code Playgroud)

但是,我对此持怀疑态度.我不明白为什么允许编译器执行此操作,因为据我所知,只有在尝试使用任意数量的读取和至少同时写入的同一内存区域时才会发生数据争用.作者继续激励:

核心问题源于编译器利用假设变量值在没有显式赋值的情况下不能异步更改的假设

在我看来,在这种情况下,条件是受到尊重的,因为局部变量my_counter永远不会被访问两次而且其他线程无法访问.编译器如何知道全局变量不能在另一个线程的另一个转换单元中设置在别处?它不能,事实上,我认为第二个if情况只会被实际优化掉.

作者是错的,还是我错过了什么?

Ext*_*t3h 8

除非counter是明确的volatile,否则编译器可能会认为如果当前执行范围内没有任何内容可以更改它,它永远不会更改.这意味着如果变量上没有别名,或者两者之间没有编译器无法知道效果的函数调用,则任何外部修改都是未定义的行为.随着volatile你将尽可能地声明外部更改,即使编译器不知道如何.

因此优化是完全有效的.事实上,即使它确实执行了复制,它仍然不会是线程安全的,因为值可能会在读取中间部分地改变,或者甚至可能完全陈旧,因为没有同步原语或原子,不能保证缓存一致性.

好吧,实际上在x86上你不会得到一个整数的中间值,至少只要它是对齐的.这是架构所保证的一个.过时的缓存仍然适用,该值可能已被另一个线程修改.

如果需要此行为,请使用互斥锁或原子.

  • @GaneaDanAndrei这不是未定义的行为.什么是未定义的行为是*一个不同的线程*来改变一个全局值.如果您有2个线程,A和B,并且只有线程B更改该值,则它自己的线程都不会观察到任何未定义的行为:A看到该值永远不会更改(因为它不会更改它),B看到该值变化(但没有看到其他任何人改变它).UB来自这两个线程的交集,其中因为A假设值没有改变,但B*确实*改变了它,可能会出现奇怪的结果. (2认同)

Xir*_*ema 6

编译器[被允许]优化假定任何"未定义的行为"根本不可能发生:程序员将阻止以调用未定义行为的方式执行代码.

这可能导致相当愚蠢的执行,例如,以下循环永远不会终止!

int vals[10];
for(int i = 0; i < 11; i++) {
    vals[i] = i;
}
Run Code Online (Sandbox Code Playgroud)

这是因为编译器知道这vals[10]将是未定义的行为,因此它假设它不会发生,并且由于它不会发生,i永远不会超过或等于11,因此该循环永远不会终止.并非所有编译器都会以这种方式积极地优化这样的循环,尽管我知道GCC会这样做.

在您正在使用的特定情况下,以这种方式读取全局变量可能是未定义的行为iff [原文如此]另一个线程可能在过渡期间修改它.因此,编译器假设跨线程修改永远不会发生(因为它是未定义的行为,并且编译器可以优化假定UB不会发生),因此重新读取值(它知道不会得到它)是完全安全的在自己的代码中修改).

解决方案是生成counteratomic(std::atomic<int>),这会强制编译器确认可能存在某种变量的跨线程操作.

  • 优化未定义行为的另一个示例:https://godbolt.org/g/rLk9wA (2认同)