【问题标题】:CMPXCHG16B correct?CMPXCHG16B 正确吗?
【发布时间】:2011-06-17 01:25:48
【问题描述】:

这似乎并不完全正确,尽管我不确定为什么。 建议会很好,因为 CMPXCHG16B 的文档非常少(我没有任何英特尔手册...)

template<>
inline bool cas(volatile types::uint128_t *src, types::uint128_t cmp, types::uint128_t with)
{
    /*
    Description:
     The CMPXCHG16B instruction compares the 128-bit value in the RDX:RAX and RCX:RBX registers 
     with a 128-bit memory location. If the values are equal, the zero flag (ZF) is set, 
     and the RCX:RBX value is copied to the memory location. 
     Otherwise, the ZF flag is cleared, and the memory value is copied to RDX:RAX.
     */
    uint64_t * cmpP = (uint64_t*)&cmp;
    uint64_t * withP = (uint64_t*)&with;
    unsigned char result = 0;
    __asm__ __volatile__ (
    "LOCK; CMPXCHG16B %1\n\t"
    "SETZ %b0\n\t"
    : "=q"(result)  /* output */ 
    : "m"(*src), /* input */
      //what to compare against
      "rax"( ((uint64_t) (cmpP[1])) ), //lower bits
      "rdx"( ((uint64_t) (cmpP[0])) ),//upper bits
      //what to replace it with if it was equal
      "rbx"( ((uint64_t) (withP[1])) ), //lower bits
      "rcx"( ((uint64_t) (withP[0]) ) )//upper bits
    : "memory", "cc", "rax", "rdx", "rbx","rcx" /* clobbered items */
    );
    return result;
}

当运行一个示例时,我得到了 0,而它应该是 1。有什么想法吗?

【问题讨论】:

    标签: c++ gcc x86-64 inline-assembly compare-and-swap


    【解决方案1】:

    以下是一些替代方案的比较:

    1. 内联汇编,如@luke h的回答。

    2. __sync_bool_compare_and_swap(): a GNU extension, gcc/clang/ICC-only, 已弃用, 编译器将发出 CMPXCHG16B 指令的伪函数至少与 -mcx16 一起

      李>
    3. atomic_compare_exchange_weak() / strong:在 C++11 中执行 atomic&lt;&gt; 的 C11 伪函数。对于 GNU,这不会在 gcc 7 及更高版本中发出CMPXCHG16B,而是调用libatomic(因此必须链接到)。动态链接的libatomic 将根据 CPU 的能力决定使用哪个版本的函数,并且在 CPU 能够CMPXCHG16B 的机器上,它将使用它。

    4. 显然clang 仍将CMPXCHG16B 内联为atomic_compare_exchange_weak()strong

    我没有尝试过机器语言,但是看看 (2) 的反汇编,它看起来很完美,我看不出 (1) 怎么能打败它。 (我对 x86 知之甚少,但编写了很多 6502。)此外,如果可以避免它,有很多建议永远不要使用汇编,并且至少可以通过 gcc/clang 避免它。所以我可以把 (1) 划掉。

    这是 gcc 版本 9.2.1 20190827 (Red Hat 9.2.1-1) (GCC) 中 (2) 的代码:

    Thread 2 "mybinary" hit Breakpoint 1, MyFunc() at myfile.c:586
    586               if ( __sync_bool_compare_and_swap( &myvar,
    => 0x0000000000407262 <MyFunc+904>:       49 89 c2        mov    %rax,%r10
       0x0000000000407265 <MyFunc+907>:       49 89 d3        mov    %rdx,%r11
    (gdb) n
    587                                                  was, new ) ) {
    => 0x0000000000407268 <MyFunc+910>:       48 8b 45 a0     mov    -0x60(%rbp),%rax
       0x000000000040726c <MyFunc+914>:       48 8b 55 a8     mov    -0x58(%rbp),%rdx
    (gdb) n
    586               if ( __sync_bool_compare_and_swap( &myvar,
    => 0x0000000000407270 <MyFunc+918>:       48 c7 c6 00 d3 42 00    mov    $0x42d300,%rsi
       0x0000000000407277 <MyFunc+925>:       4c 89 d3        mov    %r10,%rbx
       0x000000000040727a <MyFunc+928>:       4c 89 d9        mov    %r11,%rcx
       0x000000000040727d <MyFunc+931>:       f0 48 0f c7 8e 70 04 00 00      lock cmpxchg16b 0x470(%rsi)
       0x0000000000407286 <MyFunc+940>:       0f 94 c0        sete   %al
    

    然后对现实世界的算法进行 (2) 和 (3) 的锤击测试,我没有看到真正的性能差异。即使在理论上,(3) 也只有一个额外的函数调用的开销和 libatomic 包装函数中的一些工作,包括一个关于 CAS 是否成功的分支。

    (使用惰性动态链接,对 libatomic 函数的第一次调用实际上将运行一个 init 函数,该函数使用 CPUID 来检查您的 CPU 是否有cmpxchg16b。然后它将更新 PLT 存根跳过的 GOT 函数指针, 所以未来的调用将直接转到使用lock cmpxchg16blibat_compare_exchange_16_i1。名称中的i1 后缀来自GCC's ifunc mechanism 用于功能多版本控制;如果您在没有cmpxchg16b 支持的CPU 上运行它,它会将共享库函数解析为使用锁定的版本。)

    在我的真实世界的锤子测试中,函数调用开销因无锁机制保护的功能占用的 CPU 量而丢失。因此,我认为没有理由使用 __sync 函数,这些函数是特定于编译器的并且不推荐用于引导。

    这是每个 .compare_exchange_weak() 调用的 libatomic 包装器的程序集,从单步执行我的 Fedora 31 上的程序集。如果使用 -fno-plt 编译,callq *__atomic_compare_exchange_16@GOTPCREL(%rip) 将内联到调用者中,避免PLT 并在程序启动时而不是在第一次调用时尽早运行 CPU 检测。

    Thread 2 "tsquark" hit Breakpoint 2, 0x0000000000403210 in 
    __atomic_compare_exchange_16@plt ()
    => 0x0000000000403210 <__atomic_compare_exchange_16@plt+0>:     ff 25 f2 8e 02 00       jmpq   *0x28ef2(%rip)        # 0x42c108 <__atomic_compare_exchange_16@got.plt>
    (gdb) disas
    Dump of assembler code for function __atomic_compare_exchange_16@plt:
    => 0x0000000000403210 <+0>:     jmpq   *0x28ef2(%rip)        # 0x42c108 <__atomic_compare_exchange_16@got.plt>
       0x0000000000403216 <+6>:     pushq  $0x1e
       0x000000000040321b <+11>:    jmpq   0x403020
    End of assembler dump.
    (gdb) s
    Single stepping until exit from function __atomic_compare_exchange_16@plt,
    ...
    
    0x00007ffff7fab250 in libat_compare_exchange_16_i1 () from /lib64/libatomic.so.1
    => 0x00007ffff7fab250 <libat_compare_exchange_16_i1+0>: f3 0f 1e fa     endbr64
    (gdb) disas
    Dump of assembler code for function libat_compare_exchange_16_i1:
    => 0x00007ffff7fab250 <+0>:     endbr64
       0x00007ffff7fab254 <+4>:     mov    (%rsi),%r8
       0x00007ffff7fab257 <+7>:     mov    0x8(%rsi),%r9
       0x00007ffff7fab25b <+11>:    push   %rbx
       0x00007ffff7fab25c <+12>:    mov    %rdx,%rbx
       0x00007ffff7fab25f <+15>:    mov    %r8,%rax
       0x00007ffff7fab262 <+18>:    mov    %r9,%rdx
       0x00007ffff7fab265 <+21>:    lock cmpxchg16b (%rdi)
       0x00007ffff7fab26a <+26>:    mov    %r9,%rcx
       0x00007ffff7fab26d <+29>:    xor    %rax,%r8
       0x00007ffff7fab270 <+32>:    mov    $0x1,%r9d
       0x00007ffff7fab276 <+38>:    xor    %rdx,%rcx
       0x00007ffff7fab279 <+41>:    or     %r8,%rcx
       0x00007ffff7fab27c <+44>:    je     0x7ffff7fab288 <libat_compare_exchange_16_i1+56>
       0x00007ffff7fab27e <+46>:    mov    %rax,(%rsi)
       0x00007ffff7fab281 <+49>:    xor    %r9d,%r9d
       0x00007ffff7fab284 <+52>:    mov    %rdx,0x8(%rsi)
       0x00007ffff7fab288 <+56>:    mov    %r9d,%eax
       0x00007ffff7fab28b <+59>:    pop    %rbx
       0x00007ffff7fab28c <+60>:    retq
    End of assembler dump.
    

    使用 (2)不想指望他们安装正确的。我个人在源代码中下载了一个并错误地构建了它,因此 16 字节交换最终使用互斥锁:灾难。

    我没试过 (4)。或者更确切地说,我开始在代码上出现太多警告/错误,gcc 没有评论就通过了,以至于我无法在预算时间内编译它。

    请注意,虽然选项 2、3 和 4 看起来是相同的代码或几乎相同的代码应该可以工作,但实际上这三个选项都有很大不同的检查和警告,即使您有三个中的一个编译正常且没有警告在-Wall 中,如果您尝试其他选项之一,您可能会收到更多警告或错误。 __sync* 伪函数没有详细记录。 (事实上​​,文档只提到了 1/2/4/8 字节,而不是它们适用于 16 字节。同时,它们“有点”像函数模板一样工作,但你看不到模板,而且它们似乎对是否第一个和第二个 arg 类型实际上是同一类型,而 atomic_* 不是。)简而言之,比较 2、3 和 4 并不是您可能猜到的 3 分钟工作。

    【讨论】:

    • 不内联 lock cmpxchg16b 是不想将 16 字节原子对象报告为“无锁”的副作用,因为这意味着性能不存在(例如通过 lock cmpxchg16b 读取相互竞争,但我们希望无锁原子具有近乎完美的读取侧缩放)。它还在只读内存上出现段错误。所以这是一种 hack,GCC 可能还想开辟在未来某个时候改变它的可能性。没有在任何地方内联它意味着 libatomic 可以在不破坏 ABI 兼容性的情况下进行更改。 gcc.gnu.org/bugzilla/show_bug.cgi?id=84563
    • 这个答案对于正常的atomic_compare_exchange_weak 方式仍然具有极大的误导性。这意味着它每个lock cmpxchg16 运行两条cpuid 指令,这将是一场灾难。请在您有时间单步执行第二次或以后的调用并查看实际使用的定义时进行修复,或者现在删除该部分的大部分内容。甚至包括条件分支也会产生开销,因为愚蠢的 C 不能按值返回多个值,并且函数在内存中通过引用获取 arg。
    • 我在带有 GCC9.3 的 Skylake i7-6700k 上对 __int128atomic&lt;__int128&gt; 上的一个紧密循环中的单个线程进行计时。 __sync_bool_compare_and_swap 为 10M CAS 花费了大约 263M 周期,而 a128.compare_exchange_weak(a, 123); 为相同的 10M CAS 花费了大约 370M 周期。其他 128 位原子,如 .exchange() 更糟糕,在 libatomic 中的 lock cmpxchg16b 重试循环之前和之后使用 mfence,显着损害无竞争情况下的性能(比如比 __sync 慢 3 倍)。
    • 是的,对于对某些无锁数据结构进行多次原子修改的代码来说非常重要,因此无竞争的情况很常见。我假设内联汇编可以优化到与 GCC 为__sync_bool_compare_and_swap__sync_val_compare_and_swap 发出的内容大致相同。我怀疑是否值得使用内联汇编,除非您希望 valbool 都来自同一个 CAS 的结果。 (检查对expected 的更改会导致一些浪费的指令。非常小。)
    • 我编辑了您的答案以纠正一个错误:惰性动态链接不会重写 跳转本身,它会重写 PLT 中该间接跳转使用的函数指针。这就是惰性动态链接始终起作用的方式,除了 ifunc 使其运行自定义解析器函数以根据 CPUID 进行选择。与 glibc 对 memset / memcpy / strchr / strlen / 等所做的相同。我认为显示解析器函数的 asm 并不有趣或有用;这些“十几个指令”只是动态链接器代码为查找解析器函数而执行的总指令的一小部分。
    【解决方案2】:

    注意到一些问题,

    (1) 主要问题是约束,“rax”并没有做它看起来的样子,而是第一个字符“r”让 gcc 使用任何寄存器。

    (2) 不确定你的存储类型::uint128_t,但是假设x86平台的标准小端,那么高低双字也会交换。

    (3) 获取某事物的地址并将其转换为其他事物可能会破坏别名规则。取决于您的 types::uint128_t 是如何定义的,这是否是一个问题(如果它是两个 uint64_t 的结构则很好)。假设不违反别名规则,带有 -O2 的 GCC 将进行优化。

    (4) *src 应该真正标记为输出,而不是指定内存破坏器。但这实际上更多的是性能而不是正确性问题。同样 rbx 和 rcx 不需要指定为 clobbered。

    这是一个可行的版本,

    #include <stdint.h>
    
    namespace types
    {
        // alternative: union with  unsigned __int128
        struct uint128_t
        {
            uint64_t lo;
            uint64_t hi;
        }
        __attribute__ (( __aligned__( 16 ) ));
    }
    
    template< class T > inline bool cas( volatile T * src, T cmp, T with );
    
    template<> inline bool cas( volatile types::uint128_t * src, types::uint128_t cmp, types::uint128_t with )
    {
        // cmp can be by reference so the caller's value is updated on failure.
    
        // suggestion: use __sync_bool_compare_and_swap and compile with -mcx16 instead of inline asm
        bool result;
        __asm__ __volatile__
        (
            "lock cmpxchg16b %1\n\t"
            "setz %0"       // on gcc6 and later, use a flag output constraint instead
            : "=q" ( result )
            , "+m" ( *src )
            , "+d" ( cmp.hi )
            , "+a" ( cmp.lo )
            : "c" ( with.hi )
            , "b" ( with.lo )
            : "cc", "memory" // compile-time memory barrier.  Omit if you want memory_order_relaxed compile-time ordering.
        );
        return result;
    }
    
    int main()
    {
        using namespace types;
        uint128_t test = { 0xdecafbad, 0xfeedbeef };
        uint128_t cmp = test;
        uint128_t with = { 0x55555555, 0xaaaaaaaa };
        return ! cas( & test, cmp, with );
    }
    

    【讨论】:

    • 我复制并粘贴了您的代码,并在编译时使用 "g++-4.7 -g -DDEBUG=1 -std=c++0x -pthread dwcas.c -o dwcas.o -ldl -lpthread " 我得到 dwcas.c:29: Error: junk `ptr ' 表达式后。任何想法为什么?
    • 应该只是 lock cmpxchg16b %1 。在这种情况下,不需要大小,因为指令 cmpxchg16b 暗示了它。使用oword ptr 会告诉我你认为这是 MASM 汇编器,它不会与 GNU 汇编器一起汇编。
    • 我知道这个问题很老,但是在使用这个答案之前人们应该考虑一些事情。您应该认真 consider 避免使用内联汇编。除其他原因外,它真的很难做到正确。例如,这段代码几乎肯定需要一个“内存”破坏器。不是为了确保cmpxchg16b 的正确运行,而是因为目标可能是同步线程,这意味着您希望在通知其他线程之前 刷新所有寄存器。在使用 asm 之前,请考虑下面的 __sync* 答案、std::atomic 或任何其他解决方案。
    • gcc6 及更高版本将允许您使用标志输出操作数。
    【解决方案3】:

    请注意,如果您使用的是 GCC,则无需使用内联 asm 来获取此指令。您可以使用 __sync 函数之一,例如:

    template<>
    inline bool cas(volatile types::uint128_t *src,
                    types::uint128_t cmp,
                    types::uint128_t with)
    {
        return __sync_bool_compare_and_swap(src, cmp, with);
    }
    

    微软对 VC++ 也有类似的功能:

    __int64 exchhi = __int64(with >> 64);
    __int64 exchlo = (__int64)(with);
    
    return _InterlockedCompareExchange128(a, exchhi, exchlo, &cmp) != 0;
    

    【讨论】:

    • 有趣,gcc7 和后来更改为始终为 __atomic_compare_exchange 调用 16 字节 CAS 的 libatomic,但如果您使用 -mcx16 编译,已弃用的 __sync 内置函数仍将内联 lock cmpxchg16b。 (这不是 x86-64 的基准;不幸的是,早期的 K8 CPU 没有它。)。 godbolt.org/z/4bbfm6 有这个和一个使用 __atomic 和 std::atomic 的版本。如果比较失败,__sync_bool_compare_and_swap 不会更新 cmp,这与 ISO C++11 std::atomic&lt;types::uint128_t&gt;::compare_exchange_weak(或 ..._strong,它们在 x86-64 上的区别相同)不同。
    【解决方案4】:

    我得到了它为 g++ 编译的轻微变化(删除 cmpxchg16b 指令中的 oword ptr)。 但它似乎并没有按要求覆盖内存,尽管我可能错了。 [查看更新]下面给出了代码,然后是输出。

    #include <stdint.h>
    #include <stdio.h>
    
    namespace types
    {
      struct uint128_t
      {
        uint64_t lo;
        uint64_t hi;
      }
      __attribute__ (( __aligned__( 16 ) ));
     }
    
     template< class T > inline bool cas( volatile T * src, T cmp, T with );
    
     template<> inline bool cas( volatile types::uint128_t * src, types::uint128_t cmp,  types::uint128_t with )
     {
       bool result;
       __asm__ __volatile__
       (
        "lock cmpxchg16b %1\n\t"
        "setz %0"
        : "=q" ( result )
        , "+m" ( *src )
        , "+d" ( cmp.hi )
        , "+a" ( cmp.lo )
        : "c" ( with.hi )
        , "b" ( with.lo )
        : "cc"
       );
       return result;
    }
    
    void print_dlong(char* address) {
    
      char* byte_array = address;
      int i = 0;
      while (i < 4) {
         printf("%02X",(int)byte_array[i]);
         i++;
      }
    
      printf("\n");
      printf("\n");
    
    }
    
    int main()
    {
      using namespace types;
      uint128_t test = { 0xdecafbad, 0xfeedbeef };
      uint128_t cmp = test;
      uint128_t with = { 0x55555555, 0xaaaaaaaa };
    
      print_dlong((char*)&test);
      bool result = cas( & test, cmp, with );
      print_dlong((char*)&test);
    
      return result;
    }
    

    输出

    FFFFFFADFFFFFFFBFFFFFFCAFFFFFFDE
    
    
    55555555
    

    不确定输出对我是否有意义。我期待之前的价值是这样的 00000000decafbad00000feedbeef 根据结构定义。但是字节似乎在单词中分散开来。这是由于对齐指令吗?顺便说一句,CAS 操作似乎返回了正确的返回值。对破译有帮助吗?

    更新:我刚刚用 gdb 进行了一些内存检查调试。那里显示了正确的值。所以我想这一定是我的 print_dlong 程序有问题。随意纠正它。我留下这个回复,因为它有待更正,因为更正的版本将对带有打印结果的 cas 操作具有指导意义。

    【讨论】:

      【解决方案5】:

      所有英特尔文档均免费提供:Intel® 64 and IA-32 Architectures Software Developer's Manuals

      【讨论】:

      • 谢谢,我去看看。我以为只有付了。猜不到。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-09-24
      • 2015-03-24
      • 2011-05-26
      • 2023-04-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多