C++ 0x内存模型和推测性加载/存储

jan*_*neb 24 c++ concurrency memory-model compiler-optimization c++11

所以我正在阅读作为即将推出的C++ 0x标准一部分的内存模型.但是,我对允许编译器做什么的一些限制有点困惑,特别是关于推测性加载和存储的限制.

首先,一些相关的东西:

Hans Boehm关于线程和C++ 0x中的内存模型的页面

Boehm,"线程无法实现为库"

Boehm和Adve,"C++并发内存模型的基础"

Sutter,"Prism:基于原理的Microsoft本地代码平台的顺序存储模型",N2197

Boehm,"并发内存模型编译后果",N2338

现在,基本思想本质上是"数据无竞赛程序的顺序一致性",这似乎是编程简易性和允许编译器和硬件优化机会之间的妥协.如果不对不同线程对相同存储器位置的两次访问进行排序,则至少有一个存储到存储器位置,并且它们中的至少一个不是同步动作,则定义数据争用.这意味着对共享数据的所有读/写访问必须通过某些同步机制,例如互斥体或对原子变量的操作(嗯,可以对原子变量进行操作,只为专家提供放松的内存排序,但默认提供为顺序一致).

鉴于此,我对普通共享变量上的虚假或推测性加载/存储的限制感到困惑.例如,在N2338中我们有例子

switch (y) {
    case 0: x = 17; w = 1; break;
    case 1: x = 17; w = 3; break;
    case 2: w = 9; break;
    case 3: x = 17; w = 1; break;
    case 4: x = 17; w = 3; break;
    case 5: x = 17; w = 9; break;
    default: x = 17; w = 42; break;
}
Run Code Online (Sandbox Code Playgroud)

不允许编译器转换为

tmp = x; x = 17;
switch (y) {
    case 0: w = 1; break;
    case 1: w = 3; break;
    case 2: x = tmp; w = 9; break;
    case 3: w = 1; break;
    case 4: w = 3; break;
    case 5: w = 9; break;
    default: w = 42; break;
}
Run Code Online (Sandbox Code Playgroud)

因为如果y == 2,则存在对x的虚假写入,如果另一个线程同时更新x,则可能是一个问题.但是,为什么这是一个问题呢?这是一场数据竞赛,无论如何都是被禁止的; 在这种情况下,编译器只是通过写入x两次使其变得更糟,但即使单个写入也足以进行数据竞争,不是吗?即一个正确的C++ 0x程序需要同步访问x,在这种情况下,不再有数据竞争,虚假存储也不会是一个问题?

我同样对N2197中的例3.1.3以及其他一些例子感到困惑,但也许对上述问题的解释也可以解释这一点.

编辑:答案:

推测商店存在问题的原因在于,在上面的switch语句示例中,程序员可能已选择仅在y!= 2时有条件地获取锁定保护x.因此,推测商店可能会引入不存在的数据竞争原始代码,因此禁止转换.同样的论点也适用于N2197中的例3.1.3.

Ste*_*sop 8

我不熟悉你所提到的所有内容,但请注意,在y == 2的情况下,在代码的第一位,x根本没有被写入(或者就此而言是读取的).在第二位代码中,它被写入两次.这与仅写一次与写两次(至少在现有的线程模型,如pthreads)之间的区别更大.此外,存储一个本来不存储的值与仅存储一次而不是存储两次相比更有区别.出于这两个原因,您不希望编译器只更换no-op tmp = x; x = 17; x = tmp;.

假设线程A想要假设没有其他线程修改x.如果y为2,并且将值写入x,然后将其读回,它将返回其写入的值,这是合理的.但是如果线程B同时执行你的第二位代码,那么线程A可以写入x并稍后读取它,并返回原始值,因为线程B在"写入之前"保存并在"之后"恢复.或者它可以返回17,因为线程B在写入之后存储了17",并且在"线程A读取之后再次存储tmp".线程A可以执行它喜欢的任何同步,但它没有帮助,因为线程B不同步.它不同步的原因(在y == 2的情况下)是它不使用x.因此,特定位代码"使用x"的概念对于线程模型是否重要,这意味着当"不应该"时,不允许编译器更改代码以使用x.

简而言之,如果您提出的转换被允许,引入虚假写入,则永远不可能分析一些代码并得出结论它不会修改x(或任何其他内存位置).因此,有许多方便的习惯用法是不可能的,例如在没有同步的情况下在线程之间共享不可变数据.

因此,虽然我不熟悉C++ 0x对"数据竞争"的定义,但我认为它包含一些条件,允许程序员假设某个对象没有被写入,并且这种转换会违反这些条件.我推测如果y == 2,那么你的原始代码和并发代码:x = 42; x = 1; z = x在另一个线程中,没有被定义为数据竞争.或者至少如果它是数据竞争,它不是允许z以17或42结束的值.

考虑到在这个程序中,y中的值2可能用于表示"还有其他线程正在运行:不要修改x,因为我们在这里不同步,因此会引入数据竞争".也许根本就没有同步的原因是,在y的所有其他情况下,没有其他线程可以访问x.对我来说,C++ 0x希望支持这样的代码似乎是合理的:

if (single_threaded) {
    x = 17;
} else {
    sendMessageThatSafelySetsXTo(17);
}
Run Code Online (Sandbox Code Playgroud)

显然,你不希望转变为:

tmp = x;
x = 17;
if (!single_threaded) {
    x = tmp;
    sendMessageThatSafelySetsXTo(17);
}
Run Code Online (Sandbox Code Playgroud)

这与您的示例中的转换基本相同,但只有2个案例,而不是足以让它看起来像一个良好的代码大小优化.


jal*_*alf 5

如果y==2和另一个线程修改或读取x,原始样本中的竞争条件如何?这个线程永远不会触及x,所以其他线程可以自由地执行.

但是对于重新排序的版本,我们的线程会修改x,如果只是暂时的,所以如果另一个线程也操纵它,我们现在有一个竞争条件,之前没有.