【问题标题】:How do I use the LOCK ASM prefix to read a value?如何使用 LOCK ASM 前缀读取值?
【发布时间】:2011-03-21 14:11:52
【问题描述】:

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

  lock inc     [J];

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

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

它在第 2 行引发错误。

【问题讨论】:

  • 你想在这里实现什么?
  • 我要做的就是以线程安全的方式读取 [J]。

标签: assembly x86 thread-safety locking atomic


【解决方案1】:

使用XADD 或 MOV 指令代替 ADD 指令! 另见MFENCELFENCESFENCE 说明!

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

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

LOCK 前缀只能放在前面 遵循以下说明,并且仅 那些形式的指示 其中目标操作数是 内存操作数:ADD、ADC、AND、BTC、 BTR、BTS、CMPXCHG、CMPXCH8B、DEC、INC、 NEG、NOT、OR、SBB、SUB、XOR、XADD 和 XCHG.如果 LOCK 前缀与 这些说明之一和 源操作数是内存操作数, 未定义的操作码异常 (#UD) 可能 被生成。未定义的操作码 如果 LOCK 前缀用于任何 指令不在上述列表中。这 XCHG 指令总是断言 LOCK#信号不管 LOCK 前缀的存在与否

编辑2: 表格:《英特尔® 64 和 IA-32 架构软件开发人员手册,第 3A 卷》

8.1.1 保证原子操作。 Intel486 处理器(和更新的 处理器自)保证 以下基本内存操作将 始终以原子方式执行:

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

奔腾处理器(和更新的 处理器自)保证 执行额外的内存操作 将始终以原子方式执行:

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

对可缓存内存的访问 跨越总线宽度、缓存线、 和页面边界不保证 由 Intel Core 2 Duo 实现原子化, 英特尔酷睿双核、奔腾 M、奔腾 4、 Intel Xeon、P6 系列、Pentium 和 Intel486 处理器。英特尔酷睿 2 双核、英特尔酷睿双核、奔腾 M、 Pentium 4、Intel Xeon 和 P6 系列 处理器提供总线控制信号 允许外部存储器子系统 使拆分访问原子化; 然而,非对齐的数据访问将 严重影响性能 处理器,应该避免使用。

所以,为了阅读,我更喜欢使用带有 LOCK 前缀的CMPXCHG 指令,例如:

LOCK        CMPXCHG   EAX, [J]

写作:

MOV   [J], EAX
SFENCE

.

【讨论】:

  • 编译器拒绝编译第2行。甚至将其更改为:xadd eax, dword ptr J;不起作用。
  • 是的,原因是源操作数是内存操作数!
  • 感谢您澄清为什么它不能编译。所以看来我要么必须使用围栏指令,要么只是制作一个值的快照,因为它是只读处理的,所以在线程安全方面应该不是问题: mov eax, [J]; mov [JSnapshot], eax;
  • 就像它说的那样,对齐的 32 位值自动是原子的。好的!谢谢。
  • SFENCE 在这里没用,对于纯阅读,您只需要 mov 加载,在确保 J 对齐之后。 (@IamIC)。仅使用 lock cmpxchglock xadd 除非您还希望将完整的内存屏障作为同一操作的一部分。
【解决方案2】:

通常您可以确保J 充分对齐(例如自然对齐)。
那么简单的mov 对于纯加载或纯存储就足够了
在无竞争的情况下比lock-anything 更有效率。

GJ 的回答引用了英特尔手册 re: 对齐的相关部分,与 Why is integer assignment on a naturally aligned variable atomic on x86? 中的相同请注意,在 AMD 上也是原子的公共子集并不像英特尔那样宽容: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.

seq_cst store could also be donemov [J], eax + mfence,但在大多数 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-Back,不是视频 RAM 或其他东西)。

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

普通存储之后的 SFENCE 只会阻止后来的 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 功能可以使它们出错,因此您可以在不访问硬件性能计数器的情况下在虚拟机中发现此类问题。

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-01-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-19
    相关资源
    最近更新 更多