在 C++ 中使用内存屏障防止凭空值
Preventing of Out of Thin Air values with a memory barrier in C++
让我们考虑以下 C++ 中的双线程并发程序:
x,y
是全局的,r1,r2
是线程本地的,store
和 load
到 int
是原子的。
内存模型 = C++11
int x = 0, int y = 0
r1 = x | r2 = y
y = r1 | x = r2
允许编译器将其编译为:
int x = 0, int y = 0
r1 = x | r2 = 42
y = r1 | x = r2
| if(y != 42)
| x = r2 = y
而且,虽然它是线程内一致的,但它可能会导致错误的结果,因为该程序的执行可能会导致 (x, y) = (42, 42)
这叫做Out of Thin Air值问题。它存在,我们必须忍受它。
我的问题是:内存屏障是否会阻止编译器进行导致凭空值的疯狂优化?
例如:
[fence] = atomic_thread_fence(memory_order_seq_cst);
int x = 0, int y = 0
r1 = x | r2 = y
[fence] | [fence]
y = r1 | x = r2
不是单独的。在您的示例中,没有任何东西可以同步这两个线程。特别是,两个线程中的栅栏不会导致线程在该点同步;例如,您可能会得到以下序列:
(Thread #1) | (Thread #2)
r1 = x |
[fence] |
y = junk temporary |
| r2 = y // junk!
| [fence]
| x = r2
y = r1 |
避免凭空得出结果的最简单方法是使用原子整数:如果 x 和 y 是原子整数,则它们不能具有 "out of thin air" 值:
std::atomic_int x = 0, y = 0;
int r1 = x; | int r2 = y;
y = r1; | x = r2;
相关:我在 上的回答更详细地解释了 C++ 宽松原子内存模型的正式规则不排除“凭空”值。但是他们确实在注释中排除了它们。 这只是对使用 mo_relaxed
的程序进行形式验证的问题,而不是实际实现。 即使是非原子变量也是安全的,if 你避免了未定义的行为(你没有在这个问题的代码中)。
你在 x
和 y
上有数据竞争未定义行为,因为它们是非 atomic
变量,所以 C++11 标准绝对没有什么可说的允许发生。
对于没有正式内存模型的旧语言标准来说,这是相关的,人们使用 volatile
或普通 int
和编译器 + asm 障碍进行线程化,行为可能取决于在这种情况下,编译器按照您期望的方式工作。但幸运的是,“碰巧在当前实现上工作”线程的糟糕日子已经过去了。
障碍在这里没有帮助,没有任何东西可以创建同步;正如@davmac 所解释的那样,在全局操作顺序中,没有什么需要障碍来“排队”。 将屏障视为使当前线程等待其先前的部分或全部操作变为全局可见的操作;屏障不直接与其他线程交互。
凭空值是一种可能由于未定义行为而发生的事情;允许编译器对非原子变量进行软件值预测,并发明写入无论如何肯定会写入的对象。如果有一个发布存储,或者一个宽松的存储+一个屏障,编译器可能不允许在它之前发明写入,因为那可以创建
一般来说,从 C++11 语言律师的角度来看,您无法采取任何措施来确保您的程序安全(除了互斥锁或使用原子的手动锁定以防止一个线程读取 x
而另一个正在写。)
宽松的原子性足以防止编译器在没有任何其他成本的情况下发明写入。
如果您指望此变量的其他用途被积极优化,除了可能会击败自动矢量化和其他东西。
atomic_int x = 0, y = 0
r1 = x.load(mo_relaxed) | r2 = y.load(mo_relaxed)
y.store(r1, mo_relaxed) | x.store(r2, mo_relaxed)
值预测可以在线程 2 从 y
中看到该值之前推测性地将 r2
的未来值获取到管道中,但它实际上不能对其他线程可见,直到软件或者硬件肯定知道预测是正确的。 (那将是发明一种写法)。
例如允许线程 2 编译为
r2 = y.load(mo_relaxed);
if (r2 == 42) { // control dependency, not a data dependency
x.store(42, mo_relaxed);
} else {
x.store(r2, mo_relaxed);
}
但正如我所说,x = 42;
在非推测性(硬件或软件推测)之前不能对其他线程可见,因此值预测无法发明其他线程可以看到的值。 C++11 标准保证 atomics
我不知道/想不出任何机制,在 y.load
看到实际的 42 之前,其他线程实际上可以看到 42
的存储。(即 LoadStore使用后来的依赖存储重新排序负载)。不过,我不认为 C++ 标准正式保证了这一点。如果编译器能够证明 r2
在某些情况下始终为 42,并且甚至删除控制依赖性,那么也许真的是积极的线程间优化?
获取加载或释放存储绝对足以阻止因果关系违规。这不完全是 mo_consume
,因为 r2
用作值,而不是指针。
让我们考虑以下 C++ 中的双线程并发程序:
x,y
是全局的,r1,r2
是线程本地的,store
和 load
到 int
是原子的。
内存模型 = C++11
int x = 0, int y = 0
r1 = x | r2 = y
y = r1 | x = r2
允许编译器将其编译为:
int x = 0, int y = 0
r1 = x | r2 = 42
y = r1 | x = r2
| if(y != 42)
| x = r2 = y
而且,虽然它是线程内一致的,但它可能会导致错误的结果,因为该程序的执行可能会导致 (x, y) = (42, 42)
这叫做Out of Thin Air值问题。它存在,我们必须忍受它。
我的问题是:内存屏障是否会阻止编译器进行导致凭空值的疯狂优化?
例如:
[fence] = atomic_thread_fence(memory_order_seq_cst);
int x = 0, int y = 0
r1 = x | r2 = y
[fence] | [fence]
y = r1 | x = r2
不是单独的。在您的示例中,没有任何东西可以同步这两个线程。特别是,两个线程中的栅栏不会导致线程在该点同步;例如,您可能会得到以下序列:
(Thread #1) | (Thread #2)
r1 = x |
[fence] |
y = junk temporary |
| r2 = y // junk!
| [fence]
| x = r2
y = r1 |
避免凭空得出结果的最简单方法是使用原子整数:如果 x 和 y 是原子整数,则它们不能具有 "out of thin air" 值:
std::atomic_int x = 0, y = 0;
int r1 = x; | int r2 = y;
y = r1; | x = r2;
相关:我在 mo_relaxed
的程序进行形式验证的问题,而不是实际实现。 即使是非原子变量也是安全的,if 你避免了未定义的行为(你没有在这个问题的代码中)。
你在 x
和 y
上有数据竞争未定义行为,因为它们是非 atomic
变量,所以 C++11 标准绝对没有什么可说的允许发生。
对于没有正式内存模型的旧语言标准来说,这是相关的,人们使用 volatile
或普通 int
和编译器 + asm 障碍进行线程化,行为可能取决于在这种情况下,编译器按照您期望的方式工作。但幸运的是,“碰巧在当前实现上工作”线程的糟糕日子已经过去了。
障碍在这里没有帮助,没有任何东西可以创建同步;正如@davmac 所解释的那样,在全局操作顺序中,没有什么需要障碍来“排队”。 将屏障视为使当前线程等待其先前的部分或全部操作变为全局可见的操作;屏障不直接与其他线程交互。
凭空值是一种可能由于未定义行为而发生的事情;允许编译器对非原子变量进行软件值预测,并发明写入无论如何肯定会写入的对象。如果有一个发布存储,或者一个宽松的存储+一个屏障,编译器可能不允许在它之前发明写入,因为那可以创建
一般来说,从 C++11 语言律师的角度来看,您无法采取任何措施来确保您的程序安全(除了互斥锁或使用原子的手动锁定以防止一个线程读取 x
而另一个正在写。)
宽松的原子性足以防止编译器在没有任何其他成本的情况下发明写入。
如果您指望此变量的其他用途被积极优化,除了可能会击败自动矢量化和其他东西。
atomic_int x = 0, y = 0
r1 = x.load(mo_relaxed) | r2 = y.load(mo_relaxed)
y.store(r1, mo_relaxed) | x.store(r2, mo_relaxed)
值预测可以在线程 2 从 y
中看到该值之前推测性地将 r2
的未来值获取到管道中,但它实际上不能对其他线程可见,直到软件或者硬件肯定知道预测是正确的。 (那将是发明一种写法)。
例如允许线程 2 编译为
r2 = y.load(mo_relaxed);
if (r2 == 42) { // control dependency, not a data dependency
x.store(42, mo_relaxed);
} else {
x.store(r2, mo_relaxed);
}
但正如我所说,x = 42;
在非推测性(硬件或软件推测)之前不能对其他线程可见,因此值预测无法发明其他线程可以看到的值。 C++11 标准保证 atomics
我不知道/想不出任何机制,在 y.load
看到实际的 42 之前,其他线程实际上可以看到 42
的存储。(即 LoadStore使用后来的依赖存储重新排序负载)。不过,我不认为 C++ 标准正式保证了这一点。如果编译器能够证明 r2
在某些情况下始终为 42,并且甚至删除控制依赖性,那么也许真的是积极的线程间优化?
获取加载或释放存储绝对足以阻止因果关系违规。这不完全是 mo_consume
,因为 r2
用作值,而不是指针。