如何使用LOCK ASM前缀读取值?

时间:2010-07-27 12:29:18

标签: assembly locking x86 thread-safety

我知道如何使用LOCK线程安全地递增值:

  lock inc     [J];

但是我如何以线程安全的方式阅读[J](或任何值)? LOCK前缀不能与mov一起使用。如果我做以下事情:

  xor eax, eax;
  lock add eax, [J];
  mov [JC], eax;

它在第2行引发错误。

2 个答案:

答案 0 :(得分:9)

使用XADD或MOV指令代替ADD指令! 另请参阅MFENCELFENCESFENCE说明!

编辑: 如果源操作数是内存操作数,则不能将LOCK指令与ADD指令一起使用!

来自:“英特尔®64和IA-32架构软件开发人员手册”

  

LOCK前缀只能作为前缀   仅按以下说明操作   这些形式的指示   目标操作数是a   内存操作数:ADD,ADC,AND,BTC,   BTR,BTS,CMPXCHG,CMPXCH8B,DEC,INC,   NEG,NOT,OR,SBB,SUB,XOR,XADD和   XCHG。如果使用LOCK前缀   其中一条说明和   源操作数是一个内存操作数,一个   未定义的操作码异常(#UD)可以   生成。未定义的操作码   如果是,也将生成异常   LOCK前缀与any一起使用   指令不在上面的列表中。该   XCHG指令总是断言   LOCK#信号无论如何   是否存在LOCK前缀

EDIT2: 表格:“英特尔®64和IA-32架构软件开发人员手册,第3A卷”

  

8.1.1保证原子操作。   Intel486处理器(以及更新版本   处理器,因为)保证   以下基本内存操作会   总是以原子方式进行:

     
      
  • 读取或写入字节
  •   
  • 读或写一个单词   在16位边界上
  •   
  • 读取或写入在32位边界上对齐的双字
  •   
     

奔腾处理器(以及更新版本   处理器,因为)保证   跟随额外的内存操作   将始终以原子方式进行:

     
      
  • 读取或写入在64位边界上对齐的四字
  •   
  • 6位访问适合32位的未缓存内存位置   数据总线P6系列处理器
      (以及更新的处理器)
      保证以下
      额外的记忆操作将   总是以原子方式进行:
  •   
  • 未对齐16位,32位和64位访问适合的高速缓存内存   在缓存行中
  •   
     

访问可缓存的内存   分割总线宽度,缓存线,   和页面边界不保证   英特尔酷睿2双核处理原型,   Intel Core Duo,Pentium M,Pentium 4,   Intel Xeon,P6系列,Pentium和   Intel486处理器。英特尔酷睿2   Duo,Intel Core Duo,Pentium M,   Pentium 4,Intel Xeon和P6系列   处理器提供总线控制信号   允许外部存储器子系统   使拆分访问成为原子;   但是,非对齐数据访问将会   严重影响了业绩   处理器,应该避免。

因此,为了阅读,我建议使用带有LOCK前缀的CMPXCHG指令,如:

LOCK        CMPXCHG   EAX, [J]

写作:

MOV   [J], EAX
SFENSE

答案 1 :(得分:3)

通常您可以确保 J 充分对齐(例如自然对齐)。
那么普通的 mov 足以用于纯加载或纯存储
并且比 lock - 在无争议情况下的任何事情都更有效率。

GJ 的回答引用了英特尔手册 re:alignment 的相关部分,与 Why is integer assignment on a naturally aligned variable atomic on x86? 中的相同 请注意,AMD 上原子的公共子集并不像 Intel 那样宽容:AMD 可以跨越边界缩小比一个缓存行,但自然对齐的 8 字节加载/存储在两者上都是安全的。

如果您熟悉 C++11 std::atomic memory_order_acquire / _release 和 seq_cst,请参阅各种 ISA 到 asm 的映射:https://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html。或者在 https://godbolt.org/

上查看诸如 x.store(1, std::memory_order_release) 之类的编译器输出
default rel
section .bss
  align 4     ; natural alignment
J: resd 1     ; reserve 1 DWORD (NASM syntax)

section .text
   mov  eax, [J]       ; read J   (acquire semantics)

   mov  [J], eax       ; write J  (release semantics)

;;; seq_cst write J and wait for it to be globally visible before later loads (and stores, but that already happens with mov)
   xchg [J], eax       ; implicit  LOCK prefix, full memory barrier.

带有 mov [J], eax + mfenceseq_cst store could also be done,但在大多数 CPU 上通常较慢; GCC 最近切换到使用 XCHG,就像其他编译器已经做了一段时间一样。事实上,MFENCE 是如此slow on Skylake,以至于当您需要与存储区分开的屏障时,最好使用 lock or byte [rsp], 0 而不是 mfence。 (atomic_thread_fence(mo_seq_cst))


不幸的是,@GJ 建议的两部分代码都太慢了。

您也不需要 SFENCE,除非您一直在使用像 movntps [mem], xmm0 这样的 NT 商店。 (Does the Intel Memory Model make SFENCE and LFENCE redundant? 是)。 x86 的内存模型已经是程序顺序 + 带有存储转发的存储缓冲区,因此每个普通加载和普通存储都是 acquire or release operation,并且没有正常存储的 StoreStore 重新排序(对于普通内存区域,WB = Write-返回,不是视频 RAM 或其他东西)。

如果您在某些 NT 存储之后存储“数据就绪”标志(即,您希望此存储成为与那些 NT 存储相关的发布操作),并且希望您的存储成为 release operation 存储。那些较早的 NT 商店,您希望 SFENCE 在您的商店之前,以确保看到该商店的读者也将看到该线程的所有早期商店。

一个 SFENCE after 一个普通存储只会阻止后面的 NT 存储出现在它之前,但这当然不是通常的问题,即使它确实发生了。

如果您担心其他内核的可见性,请不要担心:store buffer(StoreLoad 重新排序的主要原因)已经尽可能快地将数据提交到 L1d 缓存。像 MFENCE 这样的屏障指令不会更快地使数据对其他内核可见,它们只是阻止当前线程稍后的加载/存储操作,直到早期的存储通过正常机制变得全局可见。 If I don't use fences, how long could it take a core to see another core's writes? 您通常只需要在 x86 上免费的获取/释放语义,而不是顺序一致性。


使用 lock cmpxchg 进行加载的唯一原因是您的数据未对齐。但是缓存行分割锁极其慢,比如锁定所有内核的内存访问,而不是仅仅让当前内核持有一个缓存行的独占所有权 (MESI)。有一个专门用于拆分锁的性能计数器,甚至还有一个最近的 CPU 功能可以使它们出错,因此您可以在 VM 中发现此类问题,而无需访问硬件性能计数器。

如果您不知道您的数据是否对齐,则无法保证 mov 存储是原子的,因此建议这对操作是没有意义的。如果您想要顺序一致性,那么在 store 上设置 full barrier 几乎总是更有意义,因为加载更常见并且可能非常便宜。

lock cmpxchg8b 在 32 位 x86 上可用于执行原子 8 字节加载或存储。但仅当您使用 486 时:P5 Pentium 保证对齐的 8 字节加载/存储是原子的,因此在最坏的情况下您可以使用 x87 fild / fistp 复制到堆栈上的本地. (假设 x87 FPU 设置为全精度模式,因此它可以将任何 64 位位模式转换为/从 80 位无损)。

在更新的 x86 上,即使在 32 位模式下,您也可以假设 movq xmm0, [J] / movd eax, xmm0 / 等的至少 MMX 或 SSE2 movq。这就是 gcc -m32 使用的。当然 64 位模式可以只使用 64 位整数寄存器。可以使用 lock cmpxchg16b 完成 16 字节的原子加载/存储。 (对齐的 SSE 并不保证是原子的,尽管在大多数最近的 CPU 上实际上是这样。但极端情况可能很棘手,例如 Why is integer assignment on a naturally aligned variable atomic on x86? 链接到一个多AMD K10 仅在单独插槽上的内核之间撕裂 8 字节边界。)