assembly x86上哪种写屏障更好:lock+addl还是xchgl?

xytpbqjk  于 2023-01-17  发布在  其他
关注(0)|答案(5)|浏览(351)

Linux内核使用lock; addl $0,0(%%esp)作为写屏障,而RE 2库使用xchgl (%0),%0作为写屏障,两者有什么区别,哪个更好?
x86是否也需要读屏障指令?RE 2将其读屏障功能定义为x86上的no-op,而Linux将其定义为lfence或no-op,具体取决于SSE 2是否可用。何时需要lfence

093gszye

093gszye1#

引自IA 32手册(第3A卷,第8.2章:内存排序):
在单处理器系统中,对于定义为可写回缓存的内存区域,内存排序模型遵循以下原则[...]

  • 读取不与其他读取重新排序
  • 写入不会与较早的读取重新排序
  • 对内存的写入不会与其他写入重新排序,但以下情况除外
  • 使用CLFLUSH指令执行的写入
  • 使用非临时移动指令执行的流式存储(写入)([此处列出指令])
  • 字符串操作(参见www.example.com一节8.2.4.1)
  • 读取可能与对不同位置的较早写入重新排序,但与对同一位置的较早写入不重新排序。
  • 读取或写入不能使用I/O指令、锁定指令或序列化指令重新排序
  • 读取无法通过LFENCEMFENCE指令
  • 写入不能通过SFENCEMFENCE指令

注意:上面的“在单处理器系统中”有点误导。相同的规则适用于每个(逻辑)处理器;手册接着描述了多个处理器之间的附加排序规则。2关于这个问题的唯一一点是

  • 锁定指令具有总顺序。

简而言之,只要你写回写内存(只要您不是驱动程序员或图形程序员,就可以看到所有内存),大多数x86指令几乎是顺序一致的-x86 CPU唯一能执行的重新排序是以后重新排序在写入之前执行的(独立)读取。写屏障的主要特点是它们具有lock前缀(隐式或显式),其禁止所有重新排序,并确保操作被多处理器系统中的所有处理器以相同的顺序看到。
此外,在回写内存中,读取永远不会重新排序,因此不需要读屏障。最近的x86处理器有一个较弱的内存一致性模型,用于流存储和写组合内存(通常用于Map图形内存)。这就是各种fence指令发挥作用的地方;对于任何其他内存类型,它们都不是必需的,但Linux内核中的某些驱动程序确实处理组合写入内存,因此它们只是以这种方式定义其读取屏障。IA-32手册第3A卷第11.3.1节中列出了每种内存类型的排序模型。简短版本:直写、回写和写保护允许推测性读取(遵循如上详述的规则),不可缓存和强不可缓存存储器具有强排序保证(没有处理器重新排序,立即执行读取/写入,用于MMIO),并且写入组合存储器具有弱排序(即,需要围栏的宽松排序规则)。

kx7yvsdv

kx7yvsdv2#

"锁;addl $0,0(%% esp)"在测试(%% esp)地址处的锁定变量的0状态时速度更快。因为我们将0值添加到锁定变量,并且如果地址(%% esp)处的变量的锁定值为0,则零标志将设置为1。

    • 隔离**来自英特尔数据表:

对在LFENCE指令之前发出的所有从内存加载指令执行序列化操作。此序列化操作保证在程序顺序中LFENCE指令之前的每个加载指令在LFENCE指令之后的任何加载指令全局可见之前都是全局可见的。
编者注:mfencelock ed操作是实现顺序一致性的唯一有用的隔离(在存储之后)lfence * 不会 * 阻止存储缓冲区重新排序StoreLoad。)
例如:内存写入指令如"mov"如果正确对齐,则是原子的(它们不需要锁前缀)。但此指令通常在CPU缓存中执行,此时不会对所有其他线程全局可见,因为必须首先执行内存围栏,以使此线程等待,直到其他线程可以看到以前的存储。
因此,这两条指令的主要区别在于xchgl指令不会对条件标志产生任何影响。当然,我们可以使用lock cmpxchg指令测试锁变量状态,但这仍然比使用lock add $0指令复杂。

daupos2t

daupos2t3#

lock addl $0, (%esp)mfence的替代物,而不是lfence
lock add在现代CPU上通常速度更快,尤其是Intel Skylake,它更新了微码where mfence acts like lfence as well,阻止了乱序执行,甚至寄存器上的指令。这就是为什么GCC最近在需要完整屏障时改用虚拟lock add而不是mfence。)
用例是当您需要阻止StoreLoad重新排序(x86的强内存模型允许的唯一类型),但不需要对共享变量执行原子RMW操作时。https://preshing.com/20120515/memory-reordering-caught-in-the-act/
例如,假设对准std::atomic<int> a,b

movl   $1, a             a = 1;    Atomic for aligned a
# barrier needed here between seq_cst store and later loads
movl   b, %eax           tmp = b;  Atomic for aligned b

您的选项包括:

      • 使用xchg**进行顺序一致性存储,例如mov $1, %eax/xchg %eax, a,这样就不需要单独的屏障;它是商店的一部分。我认为这是大多数现代硬件上最有效的选择;除gcc之外的C++11编译器使用xchg进行seq_cst存储。
  • 使用mfence作为屏障(gcc使用mov + mfence进行seq_cst存储)。
  • 使用lock addl $0, (%esp)作为屏障。任何lock ed指令都是完整屏障。Does lock xchg have the same behavior as mfence?

(Or到其他位置,但是栈在L1d中几乎总是私有的和热的,所以这是一个不错的选择。但是这可能会使用栈底部的数据创建一个依赖链。
您只能通过将xchg折叠到存储中来将其用作屏障,因为它无条件地向内存位置写入一个不依赖于旧值的值。
在可能的情况下,使用xchg进行seq-cst存储可能是最好的,即使它也从共享位置读取。mfence在最新的Intel CPU上比预期的要慢(加载和存储是唯一被重新排序的指令吗?),而且还像lfence一样阻止了独立的非内存指令的无序执行。
即使mfence可用,使用lock addl $0, (%esp)/(%rsp)代替mfence也是值得的,但我还没有体验过它的缺点,使用-64(%rsp)或其他东西可能会使它不太可能延长对热点(本地或返回地址)的数据依赖,但这会让valgrind这样的工具不高兴。

    • lfence对于内存排序毫无用处,除非您使用MOVNTDQA加载从视频RAM(或其他WC弱排序区域)读取。**

序列化乱序执行(但不是存储缓冲区)对于停止StoreLoad重新排序(x86的强内存模型允许正常WB(回写)内存区域的唯一类型)没有用。
lfence的实际用例是阻止rdtsc的无序执行,以便为非常短的代码块计时,或者通过条件或间接分支阻止推测来缓解Spectre。
另请参见When should I use _mm_sfence _mm_lfence and _mm_mfence(我的答案和@BeeOnRope的答案),以了解为什么lfence没有用,以及何时使用每一条barrier指令(或者在我的答案中,当用C而不是asm编程时,使用C intrinsic)。

pbwdgjma

pbwdgjma4#

除了其他答案,HotSpot开发人员发现零偏移的lock; addl $0,0(%%esp)可能不是最佳的,在一些处理器上它可以introduce false data dependencies;相关的jdk bug
在某些情况下,使用不同的偏移量接触堆栈位置可以提高性能。

9w11ddsr

9w11ddsr5#

lock; addlxchgl的重要部分是lock前缀。它对于xchgl是隐式的。两者之间实际上没有区别。我将查看它们是如何组装的,并选择较短的一个(以字节为单位),因为这对于x86上的等效操作来说通常更快(因此有xorl eax,eax这样的技巧)
SSE 2的存在可能只是真实的情况的代理,实际情况最终是cpuid的函数。结果可能是SSE 2暗示lfence的存在,并且在 Boot 时检查/缓存了SSE 2的可用性。lfence可用时需要它。

相关问题