自旋锁所需的最小 X86 组件是多少
Posted
技术标签:
【中文标题】自旋锁所需的最小 X86 组件是多少【英文标题】:What is the minimum X86 assembly needed for a spinlock 【发布时间】:2014-05-21 12:50:25 【问题描述】:在程序集中实现自旋锁。在这里,我发布了我想出的解决方案。这是正确的吗?你知道更短的吗?
锁定:
mov ecx, 0
.loop:
xchg [eax], ecx
cmp ecx, 0
je .loop
发布:
lock dec dword [eax]
eax 被初始化为 -1(这意味着锁是空闲的)。这应该适用于许多线程(不一定是 2 个)。
【问题讨论】:
注意:xor ecx, ecx
在大小和速度方面优于 mov ecx, 0
。
另一方面,您错过了第一个 xchg 中的锁定前缀。
@Polynomial 不需要 xchg 上的锁定前缀,这是隐含的。
@Jester 是的,我忘了。
***.com/a/3855824/17034
【参考方案1】:
最短的可能是:
acquire:
lock bts [eax],0
jc acquire
release:
mov [eax],0
为了性能,最好使用“测试、测试和设置”的方法,并使用pause
,像这样:
acquire:
lock bts [eax],0 ;Optimistic first attempt
jnc l2 ;Success if acquired
l1:
pause
test [eax],1
jne l1 ;Don't attempt again unless there's a chance
lock bts [eax],0 ;Attempt to acquire
jc l1 ;Wait again if failed
l2:
release:
mov [eax],0
对于调试,您可以添加额外的数据以便更轻松地检测问题,如下所示:
acquire:
lock bts [eax],31 ;Optimistic first attempt
jnc l2 ;Success if acquired
mov ebx,[CPUnumber]
lea ebx,[ebx+0x80000000]
cmp [eax],ebx ;Is the lock acquired by this CPU?
je .bad ; yes, deadlock
lock inc dword [eax+4] ;Increase "lock contention counter"
l1:
pause
test [eax],0x80000000
jne l1 ;Don't attempt again unless there's a chance
lock bts [eax],31 ;Attempt to acquire
jc l1 ;Wait again if failed
l2: mov [eax],ebx ;Store CPU number
release:
mov ebx,[CPUnumber]
lea ebx,[ebx+0x80000000]
cmp [eax],ebx ;Is lock acquired, and is CPU same?
jne .bad ; no, either not acquired or wrong CPU
mov [eax],0
【讨论】:
如果第一次尝试成功,它不会在[eax]
中存储垃圾吗? (在调试版本中)
@harold:它使用一个位来锁定,剩下的 31 位来跟踪谁获得了锁(这样它就可以检测到你何时释放了一个你没有释放的锁获取,但其他人做了;和死锁)。
是的,但我的意思是 ebx
如果你走那条路,还没有被设置为一个合理的值,对吧?
为什么是lea ebx,[ebx+0x80000000]
?这是使用不同的寄存器留下的吗?顺便说一句,CPUnumber
不应该是ThreadNumber
吗?对正确性重要的不是物理 CPU。总之,不错。这与我在Locks around memory manipulation via inline assembly 上使用xchg
的纯asm 答案基本相同。
@PeterCordes:啊,我现在明白了。我不知道(自从我写它以来已经过去了太多时间);但我有一种感觉,我不想修改标志(例如之前的测试/比较和之后的条件分支);并且(在发布之前编辑/修复示例时)它周围的代码已更改,但原始的LEA
保留了。【参考方案2】:
您的代码很好,但如果您正在寻找高性能,我建议您改为:
xor ecx, ecx
.loop:
lock xchg [eax], ecx
test ecx, ecx
jz .loop
原因:
xor ecx, ecx
更小,不需要文字,现代 CPU 已将其硬连线到快速寄存器零。
test ecx, ecx
可能比 cmp ecx, 0
更小更快,因为您不需要加载文字,而test
只是按位与运算而不是减法。
附注出于可读性原因,无论是否隐含,我总是将锁定前缀放在那里 - 这很明显我正在执行锁定操作。
【讨论】:
请注意,前缀不是免费的——它占用一个字节。这不是世界末日,但如果你注意其他指令的大小...... @gsg 我刚刚检查了我的编译器,它没有为锁定前缀添加额外的字节,因为它检测到它在 xchg 上是多余的。 嗯,好的。我检查了gcc
和as
,两者都有。我想知道你的工具。
我是 Windows 猴子,我没有 gcc! ;]
你在优化错误的东西。如果您关心性能,请不要在xchg
上旋转,在看到可用锁之前将其旋转为只读,以减少与尝试解锁的核心的争用。而pause
是必不可少的。在此处查看 Brendan 的答案,或在 Locks around memory manipulation via inline assembly@ 上查看我的答案【参考方案3】:
您的代码很好,如果您有空间问题,您可以随时尝试缩短它。
其他答案提到了性能,这表明对锁的工作原理基本无知。
当启动锁定尝试时,有问题的内核会在其一个引脚 (LOCK) 上发出一个信号,该信号会告诉所有其他内核、它们的缓存、所有内存和所有总线主控设备(因为它们可以独立更新 RAM核心)来完成任何未完成的内存操作。完成此操作后,他们共同发出另一个信号 - 锁定确认 (LOCKA) - 返回到原始内核并进行内存交换。此后,LOCK 信号关闭。
到达这里后,您可以查看使用 xchg 获取的值。如果事实证明另一个任务/线程拥有锁,您将需要重新执行锁序列。
假设您计算机上最慢的总线主控设备是 33MHz PCI 卡。如果它正在做某事,它将需要任意数量的 PCI 总线时钟周期才能完成。每个周期意味着 3.3GHz CPU 上的一百个等待周期。将此放在锁定序列中节省一两个周期的角度。 CPU、芯片组和主板中有几条总线,其中一些、全部或全部都可能在任何时候都处于活动状态 - 例如在启动 LOCK 时。使用 LOCKA 响应时间最长的活动总线将决定锁定完成的速度。
自己尝试一下:测量完成一千万个自旋锁(抓取和释放)需要多长时间。
我写了更多关于自旋锁here、here 和here。
总线锁(自旋锁,Windows 中的关键部分)的性能技巧是尽可能少地使用它们,这意味着组织数据以使其成为可能。在非服务器计算机上,总线锁可能会更快地完成。这是因为服务器上的总线主控设备或多或少地持续运行。因此,如果您的应用程序是基于服务器的,那么节省总线锁对于保持性能至关重要。
编辑
致彼得·科德斯,
你说
...它与总线主控无关,至少与 CPU 无关,因为至少 尼哈勒姆
来自最新的英特尔系统编程指南:
8.1.4 LOCK 操作对内部处理器缓存的影响
对于 Intel486 和 Pentium 处理器,LOCK# 信号总是 在 LOCK 操作期间在总线上断言,即使该区域 被锁定的内存被缓存在处理器中。
对于 P6 和更新的处理器系列,如果内存区域 在 LOCK 操作期间被锁定被缓存在处理器中 正在执行 LOCK 操作作为回写存储器并且是 完全包含在高速缓存行中,处理器可能不会断言 总线上的 LOCK# 信号。相反,它会修改内存位置 内部并允许它的缓存一致性机制,以确保 操作以原子方式执行。此操作称为“缓存 锁定。”缓存一致性机制自动防止两个或 更多的处理器缓存了相同的内存区域 同时修改该区域中的数据。
第二段说
...处理器可能不会在总线上断言 LOCK# 信号。
现在,我不了解你,但对我来说,至少“可能不会”听起来不像“不会”或“不会”。
我得出的数字可能正确,也可能不正确——即使我时常犯错误——但我挑战你不要再引用这个或那个“权威”,而是要亲自动手,要么反驳我的观点数字,找出错误或差异。我在另一个线程中包含了相关的源代码(在那里我还抱怨你崇高的、理论上的、基于扶手椅的 cmets),所以你不会花很长时间才能开始。
例如,首先证明我是
夸大在无争用情况下锁定的成本...
期待你的分析。
【讨论】:
xchg [mem], reg
在 Sandybridge 系列 CPU 上(在普通内存上,即可回写高速缓存)上具有约 25 个周期的背靠背延迟,高速缓存行在 L1d 高速缓存中保持修改状态的核心。 agner.org/optimize。多亏了 MESI,它只需要一个缓存锁,而不是一个总线锁,一个对齐的 dword 就可以完成这项工作。 (并且由于现代 x86 具有缓存一致的 DMA。)您夸大了在同一个线程重复锁定/解锁同一个锁的无争用情况下锁定的成本。
避免锁定的良好设计肯定是有帮助的,但它与总线主控无关,至少从 Nehalem 起和可能更早的 CPU 上无关。
存在“可能不”的警告,因为 misaligned lock
前缀仍然必须这样做,而且非常昂贵。 (最近有一些 CPU 功能会导致该故障,以帮助检测在性能计数器难以使用的情况下的意外使用。)我实际测试过,请参阅我在 Is it possible for two lock() statements to be executed at the same time on two CPU cores ? Do CPU cores tick at the same time? 上的答案 - 并发现在四核 Skylake 上完美缩放不同的线程在不同的位置运行xchg
。以上是关于自旋锁所需的最小 X86 组件是多少的主要内容,如果未能解决你的问题,请参考以下文章