我认为在 x86 上不存在任何真正主要/明显的性能问题的简单自旋锁就是这样的。当然,真正的互斥锁实现将使用系统调用(如 Linuxfutex http://man7.org/linux/man-pages/man2/futex.2.html)旋转一段时间后,解锁必须检查是否需要通过另一个系统调用通知任何服务员。这个很重要;你不想永远浪费 CPU 时间(和能量/热量)无所事事。但从概念上讲,这是在采取后备路径之前互斥锁的旋转部分。这是如何做到这一点的一个重要部分轻量级锁定 http://preshing.com/20111124/always-use-a-lightweight-mutex/已实施。 (在调用内核之前只尝试获取一次锁是一个有效的选择,而不是旋转。)
在内联汇编中尽可能多地实现此功能,或者最好使用 C11stdatomic
, 像这样信号量实现 https://stackoverflow.com/a/36097001/224132。这是 NASM 语法。如果使用 GNU C 内联汇编,请确保使用"memory"
破坏停止编译时内存访问重新排序 https://stackoverflow.com/questions/66855137/ttas-coherence-issue。但不要使用内联汇编;使用C_Atomic uint8_t
or C++ std::atomic<uint8_t>
with .exchange(1, std::memory_order_acquire)
and .store(0, std::memory_order_release)
, and _mm_pause()
from immintrin.h
.
;;; UNTESTED ;;;;;;;;
;;; TODO: **IMPORTANT** fall back to OS-supported sleep/wakeup after spinning some
;;; e.g. Linux futex
; first arg in rdi as per AMD64 SysV ABI (Linux / Mac / etc)
;;;;;void spin_lock (volatile char *lock)
global spin_unlock
spin_unlock:
; movzx eax, byte [rdi] ; debug check for double-unlocking. Expect 1
mov byte [rdi], 0 ; lock.store(0, std::memory_order_release)
ret
align 16
;;;;;void spin_unlock(volatile char *lock)
global spin_lock
spin_lock:
mov eax, 1 ; only need to do this the first time, otherwise we know al is non-zero
.retry:
xchg al, [rdi]
test al,al ; check if we actually got the lock
jnz .spinloop
ret ; no taken branches on the fast-path
align 8
.spinloop: ; do {
pause
cmp byte [rdi], al ; C++11
jne .retry ; if (lock.load(std::memory_order_acquire) != 1)
jmp .spinloop
; if not translating this to inline asm, you could put the spin loop *before* the function entry point, saving the last jmp
; but since this is probably too simplistic for real use, I'm going to leave it as-is.
普通存储具有发布语义,但不具有顺序一致性(您可以从 xchg 或其他东西获得)。获取/释放 https://preshing.com/20120913/acquire-and-release-semantics足以保护关键部分(因此得名)。
如果您使用原子标志位字段,您可以使用lock bts
(测试和设置)相当于 xchg-with-1。你可以旋转bt
or test
。要解锁,您需要lock btr
, 不只是btr
,因为这将是字节的非原子读取-修改-写入,甚至是包含 32 位的字节。
使用通常使用的字节或整数大小的锁,您甚至不需要lock
ed操作解锁;释放语义就足够了 https://stackoverflow.com/questions/36731166/spinlock-with-xchg/37246395#37246395。 glibc的pthread_spin_unlock http://repo.or.cz/glibc.git/blob/3f0eedddbe260aad3a7b88051d6aa2b205218aa9:/sysdeps/x86_64/nptl/pthread_spin_unlock.S它和我的解锁功能一样吗:一个简单的商店。
(lock bts
没有必要;xchg
or lock cmpxchg
对于普通锁来说同样好。)
第一次访问应该是原子 RMW
参见讨论cmpxchg 是否会在失败时写入目标缓存行?如果不是,对于自旋锁来说它比 xchg 更好吗? https://stackoverflow.com/questions/63008857/does-cmpxchg-write-destination-cache-line-on-failure-if-not-is-it-better-than- 如果第一次访问是只读的,CPU 可能只发出对该高速缓存行的共享请求。然后,如果它看到该行已解锁(希望是常见的低争用情况),则必须发送 RFO(读取所有权)才能真正能够写入缓存行。因此,这是非核心事务的两倍。
缺点是这需要MESI https://en.wikipedia.org/wiki/MESI_protocol该缓存行的独占所有权,但真正重要的是拥有锁的线程可以有效地存储0
这样我们就可以看到它已解锁。无论哪种方式,只读或 RMW,该核心都将失去该行的独占所有权,并且必须先进行 RFO,然后才能提交该解锁存储。
我认为,当多个线程排队等待已获取的锁时,只读首次访问只会优化内核之间稍微减少的流量。对此进行优化是一件愚蠢的事情。
(最快的内联组装自旋锁 https://stackoverflow.com/questions/11959374/fastest-inline-assembly-spinlock/12979828#12979828还测试了大规模竞争自旋锁的想法,其中多个线程除了尝试获取锁之外什么都不做,但结果很差。该链接的答案提出了一些不正确的主张xchg
全局锁定总线 - 对齐lock
不要这样做,只是一个缓存锁(在特定情况下递增 int 是否有效地原子? https://stackoverflow.com/questions/39393850/can-num-be-atomic-for-int-num),每个核心可以在 a 上执行单独的原子 RMW不同的同时缓存行 https://stackoverflow.com/questions/11959374/fastest-inline-assembly-spinlock/12979828#comment118186534_12979828.)
然而,如果最初的尝试发现它锁住了,我们不想继续用原子 RMW 来敲击缓存行。那就是我们回到只读状态的时候。 10 个线程全是垃圾邮件xchg
因为相同的自旋锁会使内存仲裁硬件非常繁忙。它可能会延迟解锁的存储的可见性(因为该线程必须争夺该行的独占所有权),因此它会直接适得其反。它也可以是其他核心的一般存储器。
PAUSE
也是必不可少的,以避免 CPU 对内存排序的错误推测。仅当您正在读取的内存时才退出循环was由另一个核心修改。然而,我们不想pause
在无争议的情况下。在天湖上,PAUSE
等待的时间要长得多,比如从 ~5 个周期增加到 ~100 个周期,因此您绝对应该将自旋循环与初始解锁检查分开。
我确信 Intel 和 AMD 的优化手册谈到了这一点,请参阅x86 /questions/tagged/x86标记 wiki 以及大量其他链接。
还不够好?例如,我应该使用 C 中的 register 关键字吗?
register
在现代优化编译器中是毫无意义的提示,除了调试版本(gcc -O0
).