9

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

4

5 回答 5

13

注意到几个问题,

(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 );
}
于 2011-01-28T16:12:21.223 回答
6

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

于 2011-01-28T07:54:06.837 回答
3

需要注意的是,如果您使用的是 GCC,则不需要使用内联汇编来获取此指令。您可以使用 __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;
于 2014-08-10T04:54:01.977 回答
1

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

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

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

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

  4. 显然铿锵声仍将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 函数的第一次调用实际上将运行一个使用 CPUID 来检查您的 CPU 是否具有cmpxchg16b.直接libat_compare_exchange_16_i1使用lock cmpxchg16b.i1名称中的后缀来自GCC 的 ifunc函数多版本控制机制;如果你在没有cmpxchg16b支持的 CPU 上运行它,它会将共享库函数解析为使用锁定的版本。)

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

这是每个 调用的 libatomic 包装器的程序集.compare_exchange_weak(),从单步执行我的 Fedora 31 上的程序集。如果使用 编译-fno-plt,acallq *__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 分钟的工作。

于 2020-05-13T01:40:43.643 回答
0

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

于 2014-08-10T04:33:57.743 回答