为什么我们不在 User space 中使用障碍
Why we do not use barriers in User space
我正在阅读有关内存屏障的内容,我可以总结的是它们会阻止编译器对指令进行重新排序。
所以在用户 space 记忆中可以说我有
b = 0;
main(){
a = 10;
b = 20;
c = add(a,b);
}
编译器能否重新排序此代码,以便 b = 20
赋值发生在调用 c = add()
之后。
为什么我们在这种情况下不使用障碍?我在这里错过了一些基本知识吗?
虚拟内存是否免于任何重新排序?
进一步扩展问题:
在网络驱动程序中:
1742 /*
1743 * Writing to TxStatus triggers a DMA transfer of the data
1744 * copied to tp->tx_buf[entry] above. Use a memory barrier
1745 * to make sure that the device sees the updated data.
1746 */
1747 wmb();
1748 RTL_W32_F (TxStatus0 + (entry * sizeof (u32)),
1749 tp->tx_flag | max(len, (unsigned int)ETH_ZLEN));
1750
当他说设备看到更新的数据时...如何将其与使用障碍的多线程理论联系起来。
编译器无法重新排序(运行时或 cpu 也不能),因此 b=20
位于 c=add()
之后,因为这会改变方法的语义,而事实并非如此允许的。
我会说编译器(或运行时或 cpu)按照您描述的方式行事会使行为随机,这将是一件坏事。
此重新排序限制仅适用于执行代码的线程。正如@GabrielSouthern 指出的那样,如果 a
、b
和 c
都是全局变量,则无法保证商店的顺序变得全局可见。
简答
内存屏障在用户模式代码中的使用频率低于内核模式代码,因为用户模式代码倾向于使用更高级别的抽象(例如 pthread 同步操作)。
其他详细信息
在分析可能的操作顺序时需要考虑两件事:
- 执行代码的线程将看到
中的操作的顺序
- 什么顺序其他线程将看到
中的操作
在您的示例中,编译器无法将 b=20
重新排序为在 c=add(a,b)
之后出现,因为 c=add(a,b)
操作使用 b=20
的结果。但是,编译器可能会重新排序这些操作,以便其他线程在与 b
关联的内存位置发生变化之前看到与 c
关联的内存位置发生变化。
这是否会实际发生取决于硬件实现的内存一致性模型。
至于编译器何时可能进行重新排序,您可以想象添加另一个变量如下:
b = 0;
main(){
a = 10;
b = 20;
d = 30;
c = add(a,b);
}
在这种情况下,编译器可以自由地将 d=30
赋值移动到 c=add(a,b)
之后。
但是,整个示例过于简单。程序不做任何事情,编译器可以消除所有操作,不需要向内存写入任何内容。
附录:内存重新排序示例
在多处理器环境中,多个线程可以看到以不同顺序发生的内存操作。 Intel Software Developer's Manual 在第 3 卷第 8.2.3 节中有一些示例。我在下面复制了一个屏幕截图,其中显示了一个可以重新排序加载和存储的示例。
还有一个 good blog post 提供了有关此示例的更多详细信息。
线程运行代码将始终就好像其自己的代码的源代码行的效果发生了按程序顺序。这是 好像 规则是大多数编译器优化的原因。
在单个线程中,无序的 CPUs 跟踪依赖关系使线程产生其所有指令按程序顺序执行的错觉。不过,全局可见(对其他内核上的线程)的影响可能会被其他内核乱序看到。
只有在通过共享内存与其他线程交互的代码中才需要内存屏障(作为锁定的一部分,或单独存在)。
Compilers can similarly do any reordering / hoisting they want, as long as the results are the same。 C++ 内存模型非常弱,因此即使针对 x86 CPU,编译时重新排序也是可能的。 (但当然不是在本地线程中产生不同结果的重新排序。)C11 <stdatomic.h>
和等效的 C++11 std::atomic
是告诉编译器您对运营的全球可见性。在 x86 上,这通常只会导致按源顺序放置存储指令,但默认 memory_order_seq_cst
需要在每个存储上有一个 MFENCE
以防止 StoreLoad 重新排序以实现完全顺序一致性。
在内核代码中,内存屏障也很常见,以确保存储到内存映射 I/O 寄存器的操作按要求的顺序进行。推理是相同的:对一系列存储和加载的内存的全局可见影响进行排序。区别在于观察者是一个 I/O 设备,而不是另一个 CPU 上的线程。核心通过缓存一致性协议相互交互的事实是无关紧要的。
我正在阅读有关内存屏障的内容,我可以总结的是它们会阻止编译器对指令进行重新排序。
所以在用户 space 记忆中可以说我有
b = 0;
main(){
a = 10;
b = 20;
c = add(a,b);
}
编译器能否重新排序此代码,以便 b = 20
赋值发生在调用 c = add()
之后。
为什么我们在这种情况下不使用障碍?我在这里错过了一些基本知识吗?
虚拟内存是否免于任何重新排序?
进一步扩展问题:
在网络驱动程序中:
1742 /*
1743 * Writing to TxStatus triggers a DMA transfer of the data
1744 * copied to tp->tx_buf[entry] above. Use a memory barrier
1745 * to make sure that the device sees the updated data.
1746 */
1747 wmb();
1748 RTL_W32_F (TxStatus0 + (entry * sizeof (u32)),
1749 tp->tx_flag | max(len, (unsigned int)ETH_ZLEN));
1750
当他说设备看到更新的数据时...如何将其与使用障碍的多线程理论联系起来。
编译器无法重新排序(运行时或 cpu 也不能),因此 b=20
位于 c=add()
之后,因为这会改变方法的语义,而事实并非如此允许的。
我会说编译器(或运行时或 cpu)按照您描述的方式行事会使行为随机,这将是一件坏事。
此重新排序限制仅适用于执行代码的线程。正如@GabrielSouthern 指出的那样,如果 a
、b
和 c
都是全局变量,则无法保证商店的顺序变得全局可见。
简答
内存屏障在用户模式代码中的使用频率低于内核模式代码,因为用户模式代码倾向于使用更高级别的抽象(例如 pthread 同步操作)。
其他详细信息
在分析可能的操作顺序时需要考虑两件事:
- 执行代码的线程将看到 中的操作的顺序
- 什么顺序其他线程将看到 中的操作
在您的示例中,编译器无法将 b=20
重新排序为在 c=add(a,b)
之后出现,因为 c=add(a,b)
操作使用 b=20
的结果。但是,编译器可能会重新排序这些操作,以便其他线程在与 b
关联的内存位置发生变化之前看到与 c
关联的内存位置发生变化。
这是否会实际发生取决于硬件实现的内存一致性模型。
至于编译器何时可能进行重新排序,您可以想象添加另一个变量如下:
b = 0;
main(){
a = 10;
b = 20;
d = 30;
c = add(a,b);
}
在这种情况下,编译器可以自由地将 d=30
赋值移动到 c=add(a,b)
之后。
但是,整个示例过于简单。程序不做任何事情,编译器可以消除所有操作,不需要向内存写入任何内容。
附录:内存重新排序示例
在多处理器环境中,多个线程可以看到以不同顺序发生的内存操作。 Intel Software Developer's Manual 在第 3 卷第 8.2.3 节中有一些示例。我在下面复制了一个屏幕截图,其中显示了一个可以重新排序加载和存储的示例。 还有一个 good blog post 提供了有关此示例的更多详细信息。
线程运行代码将始终就好像其自己的代码的源代码行的效果发生了按程序顺序。这是 好像 规则是大多数编译器优化的原因。
在单个线程中,无序的 CPUs 跟踪依赖关系使线程产生其所有指令按程序顺序执行的错觉。不过,全局可见(对其他内核上的线程)的影响可能会被其他内核乱序看到。
只有在通过共享内存与其他线程交互的代码中才需要内存屏障(作为锁定的一部分,或单独存在)。
Compilers can similarly do any reordering / hoisting they want, as long as the results are the same。 C++ 内存模型非常弱,因此即使针对 x86 CPU,编译时重新排序也是可能的。 (但当然不是在本地线程中产生不同结果的重新排序。)C11 <stdatomic.h>
和等效的 C++11 std::atomic
是告诉编译器您对运营的全球可见性。在 x86 上,这通常只会导致按源顺序放置存储指令,但默认 memory_order_seq_cst
需要在每个存储上有一个 MFENCE
以防止 StoreLoad 重新排序以实现完全顺序一致性。
在内核代码中,内存屏障也很常见,以确保存储到内存映射 I/O 寄存器的操作按要求的顺序进行。推理是相同的:对一系列存储和加载的内存的全局可见影响进行排序。区别在于观察者是一个 I/O 设备,而不是另一个 CPU 上的线程。核心通过缓存一致性协议相互交互的事实是无关紧要的。