函数是否是现代平台的有效内存屏障?


69

在我审阅的代码库中,我发现了以下成语。

我对我的团队的资深成员对作者说:“保存”,这里没有内存屏障!您不保证global.data将其从缓存刷新到主内存。如果线程A和线程B将在其中运行两个不同的处理器-此方案可能会失败”。

高级程序员咧嘴笑着,慢慢地解释,好像在解释他的五岁男孩如何系鞋带:“听小男孩,在高负载测试和实际客户中,我们在这里看到了许多与线程相关的错误”,他停下来挠挠他长长的胡须,“但是这个习语从来没有犯过错误”。

“但是,它在书中说……”

“安静!”他迅速向我嘘声,“也许从理论上讲不能保证,但实际上,您使用函数调用的事实实际上是内存障碍。编译器不会对指令进行重新排序global.data = data,因为它不知道是否任何在函数调用中使用它的人,x86架构将确保在线程B从管道读取命令时其他CPU将看到此全局数据,请放心,我们有很多现实问题需要担心。我们不需要在虚假的理论问题上投入额外的精力。

“请放心,我的孩子,您很快就会知道将真正的问题与我需要获得博士学位的非问题区分开来。”

他说的对吗?在实践中,这真的不是问题吗(例如x86,x64和ARM)?

这与我学到的一切相违背,但是他的确留着长胡子,而且看起来很聪明!

如果您可以向我展示一段证明他错误的代码,则可以加分!


5
他当然是对的,他从经验中知道。他可能已经提到,编写或读取管道或套接字总是涉及锁定内核,这意味着一个障碍,但是要证明给一个年轻的鞭打者要花费很多时间。
汉斯·帕桑

1
@HansPassant,但即使是那些系统调用,在病理情况下也可能最终运行在与调用它们的内核不同的内核上,从而在错误的内核上发出了内存屏障,不是吗?
mikebloch

1
@HansPassant,内核是否可以决定不时将syscall移至另一个线程以提高性能?这难道不是在系统调用之前发生的吗?
mikebloch

3
问题1:内存屏障是“将缓存刷新到主内存”还是仅保证“已完成所有写入-写入缓存”?缓存一致性机制是否不起作用来处理内核之间的缓存竞争?问题2:处理器将写入延迟多长时间?我们说的是10条机器指令还是1000条?这会继续增长吗?我问是因为在对notify()的调用中有成百上千的机器指令要执行。
johnnycrash 2012年

2
the fact you used a function call is effectively a memory barrier, the compiler will not reorder the instruction global.data = data屏障不是针对编译器的,而是针对硬件的。
developerbmw

Answers:


12

内存壁垒不仅仅是防止指令重新排序。即使不对指令进行重新排序,它仍然可能导致缓存一致性问题。至于重新排序-这取决于您的编译器和设置。ICC在重新排序方面特别具有攻击性。带有整个程序优化的MSVC也可以。

如果您的共享数据变量声明为volatile即使不在规范中,大多数编译器也会围绕该变量的读取和写入生成一个内存变量,并防止重新排序。这不是正确的使用方法volatile,也不是什么意思。

(如果我还有剩余票数,我将为您的叙述+1。)


2
但这确实是x86/中的问题x64。我可以编写一个简短的程序来证明它失败了吗?(感谢您的客气话,技术讨论应该很有趣)。
mikebloch

1
x86在缓存一致性方面做出了一些保证。x64并非如此,但实际上,英特尔意识到开发人员为x86编写了糟糕的,不安全的代码,因此即使他们没有义务并且不在规范中,他们仍会自动执行许多操作并执行高速缓存同步。但是,使用ARM时,所有赌注都没有了。请参阅这篇文章(尽管它不是x86专用的),以获取更多信息和更多口舌式叙述:ridiculousfish.com/blog/posts/barrier.html
Mahmoud Al-Qudsi 2012年

1
您是否刚刚说过,英特尔通过投资研发资源来解决他们遇到的严重问题,从而奖励那些编写错误代码并忽略文档的开发人员?可能会花费我和您的CPU效率吗?伙计,有些日子我想知道为什么我还要努力。
mikebloch

2
@mike他们尝试使用Itanic,我们都知道他们在那里取得的成功。然后AMD出现并说:“这是一个64位平台,它将运行您的x86二进制文件运行您刚刚为x64重新编译的糟糕代码,而不会解决您的错误”,因此x86_64诞生了。
马哈茂德·古德西

尽管volatile关键字不能保证内存屏障或线程安全性,但是它将保护多线程应用程序免受与编译器执行错误的优化有关的错误的影响,因为它会注意到您的线程回调函数未在代码中的任何地方调用。这在x86的现代编译器上可能不太可能发生,但在底层嵌入式编译器上则更可能发生。
伦丁2012年

9

实际上,函数调用是 编译器的障碍,这意味着编译器不会将全局内存访问移到调用之后。需要注意的是编译器知道一些功能,例如内置函数,内联函数(请注意IPO!)等。

因此,理论上需要处理器内存屏障(除了编译器屏障之外)才能完成此工作。但是,由于您要调用的读写操作会改变全局状态,因此我非常确定内核在实现这些操作时会发出内存障碍。尽管没有这样的保证,所以从理论上讲您需要障碍。


3
那么在实践中,内核模式代码==内存屏障?听起来很合理,而且听起来那个老人毕竟是对的。听起来ICC能够围绕系统调用重新排序代码,因为他不知道内核会做什么。
mikebloch

1
@janneb是的,但是在病理情况下,即使是那些系统调用也可能最终运行在与调用它们的内核不同的内核上,从而在错误的线程上发出了内存屏障。
mikebloch

1
@blaze:正如我试图在第二句话中解释的那样,该函数调用是编译器无法以某种方式窥视的,编译器必须假定可以接触全局状态。从编译器的角度来看,系统调用与共享库中的函数没有什么不同(除了函数原型之外没有可用的信息)。
詹妮布

1
据我所知,编译器不允许在调用之后更改指令执行的顺序,因为在函数参数评估之后但在调用之前存在C语言序列点。因此,在调用该函数之前,我认为必须完成编译器的指令重新排序。
伦丁

1
@Lundin:好了,编译器可以进行任何保留原始程序语义的转换(如果可以的话,可以从外部看到副作用)。因此,如果可以证明某个表达式是纯表达式(无副作用)或该副作用无关紧要,则可以忽略序列点并重新排序操作。在实践中,我不确定通过外部函数调用(不包含有关函数内部的信息)重新排序会为您带来很多好处,因此,如果编译器不理会它,也就不会感到惊讶。
janneb 2012年

2

基本规则是:编译器必须使全局状态出现与您编写的代码完全相同,但是如果它可以证明给定的函数不使用全局变量,则它可以选择任何方式实现算法。

结果是,传统的编译器始终将另一个编译单元中的函数视为内存屏障,因为它们在这些函数内部看不到。现代编译器越来越多地采用“整个程序”或“链接时间”优化策略,这些策略可以打破这些障碍,并且即使编写了好几年的代码,也会导致编写不良的代码失败。

如果所讨论的函数在共享库中,则它将无法在其中查看,但是如果该函数是C标准定义的函数,则它不需要-它已经知道该函数的作用- -所以你也要小心那些。请注意,编译器无法识别内核调用的含义,但是插入编译器无法识别的内容(内联汇编程序或对汇编程序文件的函数调用)的这种行为本身会产生内存屏障。

在您的情况下,notify它将是编译器无法在内部看到的黑匣子(库函数),否则将包含可识别的内存屏障,因此您很可能会安全。

实际上,您必须编写非常糟糕的代码才能克服这一点。


1

实际上,他是正确的,并且在这种特定情况下暗含了记忆障碍。

但是关键是,如果它的存在是“值得商bat的”,那么代码已经太复杂且不清楚。

真的,请使用互斥或​​其他适当的构造。这是处理线程和编写可维护代码的唯一安全方法。

也许您还会看到其他错误,例如,如果多次调用send(),代码将无法预测。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.