自旋锁所需的最小 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 上是多余的。 嗯,好的。我检查了gccas,两者都有。我想知道你的工具。 我是 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 组件是多少的主要内容,如果未能解决你的问题,请参考以下文章

CAS 自旋锁

Java中的自旋锁,手动实现一个自旋锁

Java中的自旋锁,手动实现一个自旋锁

自旋锁

自旋锁spinlock解析

自旋锁