在MSVC中,为什么InterlockedOr和InterlockedAnd生成循环而不是简单的锁定指令?

R. *_*ter 3 interlocked lock-free visual-c++ data-synchronization

在MSVC for x64(19.10.25019)上,

    InterlockedOr(&g, 1) 
Run Code Online (Sandbox Code Playgroud)

生成此代码序列:

    prefetchw BYTE PTR ?g@@3JC
    mov     eax, DWORD PTR ?g@@3JC                  ; g
    npad    3
$LL3@f:
    mov     ecx, eax
    or      ecx, 1
    lock cmpxchg DWORD PTR ?g@@3JC, ecx             ; g
    jne     SHORT $LL3@f
Run Code Online (Sandbox Code Playgroud)

我本来期望更简单(和无环):

    mov      eax, 1
    lock or  [?g@@3JC], eax
Run Code Online (Sandbox Code Playgroud)

InterlockedAnd生成类似的代码InterlockedOr.

必须为此指令设置循环似乎非常低效.为什么会生成此代码?

(作为旁注:我使用的全部原因是InterlockedOr对变量进行原子加载 - 我已经知道这InterlockedCompareExchange是做到这一点的方法.我很奇怪没有InterlockedLoad(&x),但我离题了... )

Cod*_*ray 5

记录的合同InterlockedOr使其返回原始值:

InterlockedOr

对指定的LONG值执行原子OR运算.该函数防止多个线程同时使用同一个变量.

LONG __cdecl InterlockedOr(
  _Inout_ LONG volatile *Destination,
  _In_    LONG          Value
);
Run Code Online (Sandbox Code Playgroud)

参数:

Destination [in,out]
指向第一个操作数的指针.该值将替换为操作结果.

[in]
第二个操作数.

返回值

该函数返回Destination参数的原始值.

这就是您所观察到的异常代码所必需的原因.编译器不能简单地发出OR指令,LOCK前缀,因为该OR指令不返回以前的值.相反,它必须LOCK CMPXCHG在循环中使用奇怪的解决方法.事实上,这个看似不寻常的序列是实现互锁操作的标准模式,当底层硬件本身不支持它们时:捕获旧值,执行与新值的互锁比较和交换,并继续尝试循环,直到此尝试的旧值等于捕获的旧值.

正如您所观察到的那样,您会看到同样的事情InterlockedAnd,原因完全相同:x86 AND指令不返回原始值,因此代码生成器必须回退涉及比较和交换的一般模式,这是直接的由硬件支持.

请注意,至少在x86上InterlockedOr实现为内在函数,优化器足够聪明,可以判断您是否使用了返回值.如果是,那么它使用涉及的变通方法代码CMPXCHG.如果你忽略了返回值,那么它会继续使用LOCK OR,就像你期望的那样发出代码.

#include <intrin.h>


LONG InterlockedOrWithReturn()
{
    LONG val = 42;
    return _InterlockedOr(&val, 8);
}

void InterlockedOrWithoutReturn()
{
    LONG val = 42;
    LONG old = _InterlockedOr(&val, 8);
}
Run Code Online (Sandbox Code Playgroud)
InterlockedOrWithoutReturn, COMDAT PROC
        mov      DWORD PTR [rsp+8], 42
        lock or  DWORD PTR [rsp+8], 8
        ret      0
InterlockedOrWithoutReturn ENDP

InterlockedOrWithReturn, COMDAT PROC
        mov          DWORD PTR [rsp+8], 42
        prefetchw    BYTE PTR [rsp+8]
        mov          eax, DWORD PTR [rsp+8]
LoopTop:
        mov          ecx, eax
        or           ecx, 8
        lock cmpxchg DWORD PTR [rsp+8], ecx
        jne          SHORT LoopTop
        ret          0
InterlockedOrWithReturn ENDP
Run Code Online (Sandbox Code Playgroud)

优化器同样适用于智能InterlockedAnd,也应该用于其他Interlocked*功能.

直觉会告诉你,LOCK OR实现比LOCK CMPXCHG循环更有效.不仅存在扩展的代码大小和循环的开销,而且还存在分支预测未命中的风险,这可能会花费大量的周期.在性能关键代码中,如果您可以避免依赖互锁操作的返回值,则可以获得性能提升.

但是,你真正应该在现代C++中使用的是std::atomic,它允许你指定所需的内存模型/语义,然后让标准库维护者处理复杂性.

  • 它实际上确实提供了这种保证,@ R.Alabaster.在x86上,只要您从内存中读取的值对齐,`MOV`指令就足以实现顺序一致的原子加载.这几乎总是如此,并且编译器知道它在您的测试用例中,因此它会发出最简单的代码. (2认同)