C++ std::atomic的锁在哪里?

C++ std::atomic的锁在哪里?,c++,c++11,x86,atomic,stdatomic,C++,C++11,X86,Atomic,Stdatomic,如果一个数据结构中有多个元素,那么它的原子版本不能(始终)是无锁的。 有人告诉我,对于较大的类型,这是正确的,因为CPU不能在不使用某种锁的情况下自动更改数据 例如: #include <iostream> #include <atomic> struct foo { double a; double b; }; std::atomic<foo> var; int main() { std::cout << var.i

如果一个数据结构中有多个元素,那么它的原子版本不能(始终)是无锁的。 有人告诉我,对于较大的类型,这是正确的,因为CPU不能在不使用某种锁的情况下自动更改数据

例如:

#include <iostream>
#include <atomic>

struct foo {
    double a;
    double b;
};

std::atomic<foo> var;

int main()
{
    std::cout << var.is_lock_free() << std::endl;
    std::cout << sizeof(foo) << std::endl;
    std::cout << sizeof(var) << std::endl;
}
由于原子和
foo
大小相同,我认为原子中没有存储锁

我的问题是:

如果一个原子变量使用锁,那么它存储在哪里?这对该变量的多个实例意味着什么?

回答此类问题的最简单方法通常是查看生成的程序集并从中获取它

编译以下内容(我将您的结构放大以躲避狡猾的编译器诡计):

很好,编译器委托给一个内在的(
\uuu原子存储
),它没有告诉我们这里到底发生了什么。但是,由于编译器是开源的,我们可以很容易地找到内在的实现(我在中找到它):

似乎神奇的事情发生在
锁定指针()
,让我们来看看:

static __inline Lock *lock_for_pointer(void *ptr) {
  intptr_t hash = (intptr_t)ptr;
  // Disregard the lowest 4 bits.  We want all values that may be part of the
  // same memory operation to hash to the same value and therefore use the same
  // lock.  
  hash >>= 4;
  // Use the next bits as the basis for the hash
  intptr_t low = hash & SPINLOCK_MASK;
  // Now use the high(er) set of bits to perturb the hash, so that we don't
  // get collisions from atomic fields in a single object
  hash >>= 16;
  hash ^= low;
  // Return a pointer to the word to use
  return locks + (hash & SPINLOCK_MASK);
}

下面是我们的解释:原子的地址用于生成一个散列键来选择一个预先分配的锁。

通常的实现是一个互斥体的散列表(或者甚至只是一个简单的自旋锁,没有回退到操作系统辅助的睡眠/唤醒),使用原子对象的地址作为键。散列函数可能很简单,只需使用地址的低位作为2次方大小数组的索引,但是@Frank的回答显示LLVM的std::atomic实现在某些高位执行XOR,因此当对象被2次方大的分隔时,不会自动获得混叠(这比任何其他随机排列更常见)

我想(但我不确定)g++和clang++是ABI兼容的;也就是说,它们使用相同的哈希函数和表,因此它们在哪个锁序列化对哪个对象的访问上达成一致。但是,锁定都是在
libatomic
中完成的,因此如果动态链接
libatomic
,那么调用
\u atomic\u store\u 16
w的同一程序中的所有代码我将使用相同的实现;clang++和g++明确同意调用哪个函数名,这就足够了。(但请注意,只有不同进程之间共享内存中的无锁原子对象才能工作:每个进程都有自己的锁哈希表。。无锁对象应该(事实上也是如此)只需在普通CPU架构的共享内存中工作,即使该区域映射到不同的地址。)

散列冲突意味着两个原子对象可能共享同一个锁。这不是一个正确性问题,但可能是一个性能问题:不是两对线程分别为两个不同的对象争用,而是所有4个线程都争用任何一个对象的访问权。可能是这样的这是不寻常的,通常你的目标是让你的原子对象在你关心的平台上无锁。但大多数时候你并不是真的不走运,而且基本上没问题

死锁是不可能的,因为没有任何
std::atomic
函数试图同时锁定两个对象。因此,获取锁的库代码在持有其中一个锁时从不尝试获取另一个锁。额外的争用/序列化不是正确性问题,而是性能问题


带有GCC和MSVC的x86-64 16字节对象

作为一种技巧,编译器可以使用
锁cmpxchg16b
来实现16字节的原子加载/存储,以及实际的读-修改-写操作

这比锁定要好,但与8字节的原子对象(例如纯负载与其他负载竞争)相比,性能较差。这是唯一一种记录在案的使用16字节进行原子操作的安全方法1

顺便说一句,MSVC从不为16字节的对象使用
lock cmpxchg16b
,它们基本上与24或32字节的对象相同

当您使用
-mcx16
编译时,gcc6和早期版本是内联的(不幸的是,cmpxchg16b不是x86-64的基线;第一代AMD K8 CPU缺少它。)

gcc7决定始终调用
libatomic
,并且从不将16字节对象报告为无锁对象,即使libatomic函数在有指令的机器上仍然使用
lock cmpxchg16b
。请参阅。解释此更改的gcc邮件列表消息

您可以使用union hack在x86-64上使用gcc/clang:.
lock cmpxchg16b
获得一个相当便宜的ABA指针+计数器,用于指针和计数器的更新,但只需加载指针即可。但这仅适用于使用
lock cmpxchg16b
的16字节对象实际上是无锁的情况


脚注1
movdqa
16字节加载/存储实际上在某些(但不是全部)x86微体系结构上是原子的,并且没有可靠或有文档记录的方法来检测它何时可用。请参阅,举个例子,其中K10 Opteron显示仅在具有HyperTransport的套接字之间的8B边界处撕裂


因此,编译器编写人员必须谨慎行事,不能像使用SSE2
movq
来处理8字节原子加载/存储在32位代码中那样使用
movdqa
。如果CPU供应商能够为某些微体系结构提供一些保证,或者为原子16、32和64字节对齐向量加载/s添加CPUID功能位,那就太好了撕毁(用SSE、AVX和AVX512)。也许在Muko许多套接字机器上,固件不能使用固件,而不是使用专门的一致性的胶芯片,它不直接传输整个高速缓存线。

< P>从C++标准的27.5.9:

注意:原子专门化的表示不需要 与其对应的参数类型大小相同。应指定专门化 有两个
#include <atomic>

struct foo {
    double a;
    double b;
    double c;
    double d;
    double e;
};

std::atomic<foo> var;

void bar()
{
    var.store(foo{1.0,2.0,1.0,2.0,1.0});
}
bar(): # @bar()
  sub rsp, 40
  movaps xmm0, xmmword ptr [rip + .LCPI0_0] # xmm0 = [1.000000e+00,2.000000e+00]
  movaps xmmword ptr [rsp], xmm0
  movaps xmmword ptr [rsp + 16], xmm0
  movabs rax, 4607182418800017408
  mov qword ptr [rsp + 32], rax
  mov rdx, rsp
  mov edi, 40
  mov esi, var
  mov ecx, 5
  call __atomic_store
void __atomic_store_c(int size, void *dest, void *src, int model) {
#define LOCK_FREE_ACTION(type) \
    __c11_atomic_store((_Atomic(type)*)dest, *(type*)dest, model);\
    return;
  LOCK_FREE_CASES();
#undef LOCK_FREE_ACTION
  Lock *l = lock_for_pointer(dest);
  lock(l);
  memcpy(dest, src, size);
  unlock(l);
}
static __inline Lock *lock_for_pointer(void *ptr) {
  intptr_t hash = (intptr_t)ptr;
  // Disregard the lowest 4 bits.  We want all values that may be part of the
  // same memory operation to hash to the same value and therefore use the same
  // lock.  
  hash >>= 4;
  // Use the next bits as the basis for the hash
  intptr_t low = hash & SPINLOCK_MASK;
  // Now use the high(er) set of bits to perturb the hash, so that we don't
  // get collisions from atomic fields in a single object
  hash >>= 16;
  hash ^= low;
  // Return a pointer to the word to use
  return locks + (hash & SPINLOCK_MASK);
}