繁体   English   中英

CMPXCHG16B 正确吗?

[英]CMPXCHG16B correct?

这似乎并不完全正确,尽管我不确定为什么。 建议会很好,因为 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。有什么想法吗?

注意到几个问题,

(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 );
}

所有英特尔文档均可免费获得:英特尔® 64 位和 IA-32 架构软件开发人员手册

值得注意的是,如果您使用 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;

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

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

  2. __sync_bool_compare_and_swap() :GNU 扩展,gcc/clang/ICC-only,不推荐使用的伪函数,编译器将至少使用-mcx16发出CMPXCHG16B指令

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

  4. 显然铛仍将在线CMPXCHG16Batomic_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 函数指针,因此以后的调用将直奔libat_compare_exchange_16_i1使用lock cmpxchg16b的。 i1的后缀名是从GCC的ifunc机制的功能,多版本,如果你运行它,而不在CPU上cmpxchg16b支持,就解决了共享库函数的版本,使用锁定.)

在我真实世界的锤子测试中,该函数调用开销会因无锁机制所保护的功能占用的 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) 的唯一好处是,如果您的机器没有附带libatomic (旧的 Red Hat 是这样)并且您无法要求系统管理员提供此功能或不想计算在他们安装正确的。 我个人在源代码中下载了一个并错误地构建了它,因此 16 字节交换最终使用了互斥锁:灾难。

我没试过(4)。 或者更确切地说,我开始对 gcc 传递的代码发出如此多的警告/错误而没有评论,以至于我无法在预算时间内对其进行编译。

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

我把它编译为 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 操作有指导意义。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM