GCC对读/写指令的重新排序

时间:2014-02-28 22:13:25

标签: memory gcc linux-kernel cpu compiler-optimization

Linux的同步原语(自旋锁,互斥锁,RCU)使用内存屏障指令强制重新排序内存访问指令。这种重新排序可以由CPU本身或编译器完成。

有人可以展示一些GCC生成的代码示例吗?我主要对x86感兴趣。我之所以问这个问题,是为了理解GCC如何决定可以重新排序的指令。不同的x86 mirco架构(例如:沙桥与常春藤桥)使用不同的缓存架构。因此,我想知道GCC如何进行有效的重新排序,无论缓存架构如何,都有助于执行性能。一些示例C代码和重新排序的GCC生成的代码将非常有用。谢谢!

2 个答案:

答案 0 :(得分:20)

GCC可能做的重新排序与(x86)CPU可能重新排序无关。

让我们从编译器重新排序开始。 C语言规则是禁止GCC重新排序volatile加载并相互存储内存访问,或者删除它们,当它们之间出现序列点时(感谢{ {3}}为此澄清)。也就是说,在汇编输出中,将出现那些存储器访问,并将按照您指定的顺序精确排序。另一方面,非volatile次访问可以针对所有其他访问进行重新排序,volatile或不进行,前提是(通过as-if规则)计算的最终结果是相同。

例如,C代码中的非volatile加载可以像代码所说的那样多次完成,但是顺序不同(例如,如果编译器认为在早期或稍后执行它更方便当有更多的寄存器时)。它可以比代码所说的少做几次(例如,如果值的副本在大表达式的中间仍然可用于寄存器中)。或者它甚至可以被删除(例如,如果编译器可以证明加载的无用性,或者它是否将变量完全移入寄存器中)。

要在其他时间阻止编译器重新排序,必须使用特定于编译器的屏障。 GCC为此目的使用__asm__ __volatile__("":::"memory");

这与 CPU重新排序,a.k.a。内存订购模式不同。古代CPU按照它们在程序中出现的顺序执行指令;这称为程序排序,或强内存排序模型。然而,现代CPU有时会通过削弱一点内存模型来使用“作弊”来更快地运行。

x86 CPU削弱内存模型的方式记录在英特尔软件开发人员手册第3卷第8章第8.2.2节“P6中的内存排序和更新的处理器系列”中。这部分是它的内容:

  • 读取不会与其他读取重新排序。
  • 写入不会与较旧的读取重新排序。
  • 写入内存不会与其他写入重新排序,[某些]例外。
  • 可以使用较旧的写入对不同位置进行重新排序,但不能将较旧的写入重新排序到同一位置。
  • 读取或写入不能使用I / O指令,锁定指令或序列化指令重新排序。
  • 读取不能通过早期的LFENCE和MFENCE指令。
  • 写入不能通过早期的LFENCE,SFENCE和MFENCE指令。
  • LFENCE指令无法通过早期读取。
  • SFENCE指令无法通过更早的写入。
  • MFENCE指令无法通过早期的读取或写入。

它还提供了可以和不可以重新排序的非常好的例子,在第8.2.3节“说明内存订购原则的例子”

正如您所看到的,可以使用FENCE指令来防止x86 CPU不正确地重新排序内存访问。

最后,您可能会对bobc链接感兴趣,该链接会进一步详细说明,并附带您渴望的汇编示例。

答案 1 :(得分:3)

  • C语言规则禁止GCC重新排序易失性加载并相互存储内存访问,或删除它们。

事实并非如此,而且具有误导性。 C规格提供此类保证。 见When is a Volatile Object Accessed?

  

该标准鼓励编译器避免对易失性对象的访问进行优化,但是将其实现定义为构成易失性访问的内容。最低要求是在序列点处对易失性对象的所有先前访问已经稳定并且没有发生后续访问。因此,实现可以自由地重新排序和组合在序列点之间发生的易失性访问,但不能对序列点上的访问进行组合。

传统上,程序员依赖volatile作为一种廉价的同步方法,但这不再是一种可靠的方法。