我知道如何使用LOCK线程安全地递增值:
lock inc [J];
但是我如何以线程安全的方式阅读[J](或任何值)? LOCK前缀不能与mov一起使用。如果我做以下事情:
xor eax, eax;
lock add eax, [J];
mov [JC], eax;
它在第2行引发错误。
答案 0 :(得分:9)
使用XADD或MOV指令代替ADD指令! 另请参阅MFENCE,LFENCE和SFENCE说明!
编辑: 如果源操作数是内存操作数,则不能将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
+ mfence
的 seq_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 字节边界。)