问题 在x64 CPU上读取原子16字节


我需要原子地读/写16个字节。我只使用cmpxchg16进行写入,cmpxchg16可以在所有x64处理器上使用,除了我认为对于一个不起眼的AMD处理器。

现在的问题是对齐的16字节值,只使用cmpxchg16进行修改(它就像一个完整的内存屏障)是否有可能读取一个半字节数据和一半新数据的16字节位置?

只要我用SSE指令读取(因此线程不能在读取过程中被中断),我认为读取看不一致的数据是不可能的(即使在多处理器numa系统中)。我认为它必须是原子的。

我假设当执行cmpxchg16时,它会原子地修改16个字节,而不是通过编写两个8字节块,其他线程可能在其间进行读取(老实说,我不知道它是如何工作的,如果它不是原子的。)

我对吗?如果我错了,有没有办法在不诉诸锁定的情况下进行原子16字节读取?

注意:有一个 这里有类似的问题 但是他们没有处理只用cmpxchg16进行写操作的情况,所以我觉得这是一个单独的,没有答案的问题。

编辑: 其实我认为我的推理是错误的。 SSE加载指令可以作为两个64位读取执行,并且cmpxchg16可以在另一个处理器的两次读取之间执行。


6465
2018-03-15 19:15


起源

在链接的问题中已经回答了16字节SSE读取可以用多个存储器访问来实现,即它们不是原子的。使用CMPXCHG16B以原子方式完成写入没有任何区别。读取也必须是原子的,否则您可能会看到不一致的数据。 AFAIK您唯一的选择是阅读CMPXCHG16B。 - Timo
是的,我错误地认为我只需要阻止线程在读取之间被中断,但实际的总线操作本身仍然可以交错。 - Eloff
在读取上使用cmpxchg16b会使它们放慢速度,令人无法接受。但是通过使用25%以上的内存,我可以像Dmitry Vyukov的hashmap那样进行seqlock样式的处理: 1024cores.net/home/downloads - Eloff


答案:


typedef struct
{
  unsigned __int128 value;
} __attribute__ ((aligned (16))) atomic_uint128;

unsigned __int128
atomic_read_uint128 (atomic_uint128 *src)
{
  unsigned __int128 result;
  asm volatile ("xor %%rax, %%rax;"
                "xor %%rbx, %%rbx;"
                "xor %%rcx, %%rcx;"
                "xor %%rdx, %%rdx;"
                "lock cmpxchg16b %1" : "=A"(result) : "m"(*src) : "rbx", "rcx");
  return result;
}

这应该够了吧。 typedef确保正确对齐。该 cmpxchg16b 需要数据在16字节边界上对齐。

cmpxchg16b将测试是否 *src 包含零,如果是,则写入零(nop)。在任何一种情况下,正确的值将在RAX:RDX之后。

上面的代码评估为简单的事情

push   %rbx
xor    %rax,%rax
xor    %rbx,%rbx
xor    %rcx,%rcx
xor    %rdx,%rdx
lock cmpxchg16b (%rdi)
pop    %rbx
retq

9
2018-03-15 19:32



是的,我认为这必须是这样做的方式。现在我发现一个简单的SSE加载可以分成两个64位读取,并且cmpxchg16可能会在读取之间发生。 - Eloff


根据参考文献 http://siyobik.info/main/reference/instruction/CMPXCHG8B%2FCMPXCHG16B 该 CMPXCHG16 默认情况下不是原子,但可以通过使用原子 LOCK  http://siyobik.info/main/reference/instruction/LOCK

这意味着默认情况下,可以在读写阶段更改数据。锁定使读取和写入都成为原子。


1
2018-03-15 19:23



“请注意,CMPXCHG16B要求目标(内存)操作数为16字节对齐。” - kay
对不起,是的,我的意思是带锁前缀的cmpxchg16。但锁不能与SSE指令一起使用。 - Eloff