std :: atomic的锁在哪里?

如果数据结构中包含多个元素,则其原子版本不能(始终)无锁定。 有人告诉我,对于较大的类型,这是真的,因为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;
}

输出(Linux / gcc)是:

0
16
16

由于atomic和foo的大小相同,我不认为锁存储在原子中。

我的问题是:
如果一个原子变量使用一个锁,那么它的存储位置是什么,这对变量的多个实例意味着什么?


回答这些问题的最简单方法通常是查看生成的程序集并从中取出。

编译以下内容(我将你的结构放大以避免狡猾的编译器shenanigans):

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

在铿锵5.0.0在-O3下产生以下内容:参见godbolt

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

太好了,编译器委托给一个内在的( __atomic_store ),这并没有告诉我们这里到底发生了什么。 但是,由于编译器是开源的,我们可以很容易地找到内在的实现(我在https://github.com/llvm-mirror/compiler-rt/blob/master/lib/builtins/atomic.c中找到它):

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

看起来像是在lock_for_pointer()发生了lock_for_pointer() ,所以让我们来看看它:

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

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


通常的实现是使用原子对象的地址作为关键字的互斥量散列表(甚至只是简单的自旋锁,而没有回退到OS辅助的睡眠/唤醒) 。 哈希函数可能很简单,只需要使用地址的低位作为2的幂等大小的数组的索引,但@弗兰克的答案显示,LLVM的std :: atomic实现在某些更高位执行XOR,当物体被2的大幂分开(这比任何其他随机排列更常见)时,它会自动得到混叠。

我认为(但我不确定)g ++和clang ++是ABI兼容的; 即他们使用相同的散列函数和表,所以他们同意哪个锁序列化访问哪个对象。 但是,锁定全部在libatomic完成,所以如果您动态链接libatomic那么调用__atomic_store_16的同一程序中的所有代码将使用相同的实现; 铿锵声++和g ++完全同意调用哪个函数名,这就够了。 (但是请注意, 只有在不同进程之间的共享内存中的无锁原子对象才能工作:每个进程都有自己的哈希表锁 。无锁对象应该(并且事实上)在普通CPU上的共享内存中工作体系结构,即使该区域映射到不同的地址。)

哈希碰撞意味着两个原子对象可能共享相同的锁。 这不是一个正确性问题,但它可能是一个性能问题 :不是两对线程分别为两个不同的对象而彼此竞争,您可以让所有4个线程竞争访问任一对象。 推测这是不寻常的,通常你的目标是让你的原子对象在你关心的平台上无锁。 但大多数时候你不会很不走运,而且基本没问题。

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


使用GCC与MSVC的x86-64 16字节对象

作为破解,编译器可以使用lock cmpxchg16b来实现16字节的原子加载/存储,以及实际的读取 - 修改 - 写入操作。

这比锁定要好,但与8字节的原子对象(例如纯负载与其他负载相竞争)相比性能较差。 这是以16字节1自动完成任何事情的唯一有记录的安全方式1。

AFAIK,MSVC从不使用lock cmpxchg16b作为16字节的对象,它们基本上与24或32字节的对象相同。

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

gcc7决定总是调用libatomic并且从不报告16字节的对象作为无锁,尽管libatomic函数仍然会在指令可用的机器上使用lock cmpxchg16b 。 在升级到MacPorts gcc 7.3后,请参阅is_lock_free()返回false。 解释这种改变的gcc邮件列表消息就在这里。

你可以使用union hack在x86-64上用gcc / clang获得一个相当便宜的ABA指针+计数器:我如何用c ++ 11 CAS来实现ABA计数器? lock cmpxchg16b用于更新指针和计数器,但只是指针的简单mov加载。 这只有在使用lock cmpxchg16b ,16字节的对象实际上是无lock cmpxchg16b


脚注1 :在一些(但不是全部)x86微架构中, movdqa 16字节加载/存储在实践中是原子性的,并且没有可靠的或记录的方式来检测它何时可用。 请参阅为什么x86上的自然对齐变量原子上的整数赋值以及SSE指令:哪些CPU可以执行原子16B内存操作? 例如K10 Opteron显示仅在具有HyperTransport的插槽之间的8B边界处撕裂。

因此,编译器编写者必须谨慎行事,不能movdqa使用SSE2 movq那样使用movdqa来处理32位代码中的8字节原子加载/存储。 如果CPU供应商可以为某些微架构提供某些保证,或者为原子16,32和64字节对齐的矢量加载/存储(使用SSE,AVX和AVX512)添加CPUID特征位,那将会很棒。 也许哪些主板供应商可以在使用特殊一致性胶水芯片的时髦多插槽机器上的固件上禁用,而这些芯片不会自动传输整个缓存行。


从C ++标准的29.5.9开始:

注意:原子专业化的表示形式不必与其对应的参数类型具有相同的大小。 只要有可能,专业化应该具有相同的大小,因为这样可以减少移植现有代码所需的工作量。 - 结束笔记

尽管不是必要的,但最好使原子的大小与其参数类型的大小相同。 达到此目的的方法是避免锁定或将锁存储在单独的结构中。 正如其他答案已经清楚解释的那样,哈希表被用来保存所有的锁。 这是为所有正在使用的原子对象存储任意数量的锁的最有效的内存方式。

链接地址: http://www.djcxy.com/p/63943.html

上一篇: Where is the lock for a std::atomic?

下一篇: method's arguments not being passed to decorator