簡體   English   中英

std :: atomic的鎖在哪里?

[英]Where is the lock for a 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

由於原子和foo的大小相同,我不認為鎖存儲在原子中。

我的問題是:
如果一個原子變量使用一個鎖,它存儲在哪里,這對該變量的多個實例意味着什么?

通常的實現是使用原子對象的地址作為鍵,互斥體的哈希表(甚至只是簡單的自旋鎖,沒有回退到OS輔助的睡眠/喚醒) 哈希函數可能就像使用地址的低位作為2次冪大小的數組的索引一樣簡單,但是@Frank的答案顯示LLVM的std :: atomic實現在某些更高的位中進行異或運算所以你不要當對象被2的大功率分開時,t會自動獲得混疊(這比任何其他隨機排列更常見)。

我認為(但我不確定)g ++和clang ++是ABI兼容的; 即他們使用相同的散列函數和表,因此他們同意哪個鎖序列化訪問哪個對象。 鎖定都是在libatomic完成的,所以如果你動態鏈接libatomic那么調用__atomic_store_16的同一個程序中的所有代碼__atomic_store_16將使用相同的實現; clang ++和g ++肯定同意調用哪些函數名,這就足夠了。 (但請注意, 在不同進程之間的共享內存只有無鎖原子對象才有效:每個進程都有自己的鎖定哈希表 。無鎖對象應該(實際上是)只需在普通CPU的共享內存中工作體系結構,即使該區域映射到不同的地址。)

散列沖突意味着兩個原子對象可能共享同一個鎖。 這不是一個正確性問題,但它可能是一個性能問題 :您可以讓所有4個線程爭用訪問任一對象,而不是兩個線程分別相互競爭兩個不同的對象。 大概這是不尋常的,通常你的目標是你的原子對象在你關心的平台上無鎖。 但大多數時候你並沒有真正走運,而且基本上沒問題。

死鎖是不可能的,因為沒有任何std::atomic函數試圖同時鎖定兩個對象。 因此,獲取鎖的庫代碼永遠不會嘗試在持有其中一個鎖的同時獲取另一個鎖。 額外爭用/序列化不是正確性問題,只是性能問題。


x86-64 GCC與MSVC的16字節對象

作為一個hack,編譯器可以使用lock cmpxchg16b來實現16字節的原子加載/存儲,以及實際的讀 - 修改 - 寫操作。

這比鎖定更好,但與8字節原子對象相比具有不良性能(例如純負載與其他負載競爭)。 它是唯一一個以16字節1自動執行任何操作的安全方法。

AFAIK,MSVC永遠不會將lock cmpxchg16b用於16字節對象,它們基本上與24或32字節對象相同。

gcc6和早期內聯lock cmpxchg16b當你編譯-mcx16 (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字節對象。


腳注1movdqa 16字節加載/存儲在某些(但不是全部)x86微體系結構中實際上是原子的,並且沒有可靠或記錄的方法來檢測它何時可用。 請參閱為什么在x86上對自然對齊的變量進行整數賦值? SSE指令:哪些CPU可以進行原子16B內存操作? 例如,K10 Opteron只顯示在具有HyperTransport的套接字之間的8B邊界處撕裂。

因此編譯器編寫者必須小心謹慎,並且不能movdqa在32位代碼中使用SSE2 movq進行8字節原子加載/存儲一樣使用movdqa 如果CPU供應商可以記錄某些微體系結構的某些保證,或者為原子16,32和64字節對齊的向量加載/存儲(使用SSE,AVX和AVX512)添加CPUID功能位,那將是很好的。 也許哪些主板供應商可以在使用特殊一致性膠水芯片的時髦多插槽機器上的固件中禁用,這些芯片不會原子地傳輸整個緩存線。

回答這些問題的最簡單方法通常是查看生成的裝配並從那里取出。

編譯以下內容(我使你的結構更大,以躲避狡猾的編譯器惡作劇):

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

在clang 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() ,所以讓我們來看看它:

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

這里是我們的解釋:原子的地址用於生成一個哈希鍵來選擇一個預先分配的鎖。

從C ++標准的29.5.9開始:

注意:原子特化的表示不必與其對應的參數類型具有相同的大小。 專業化應盡可能具有相同的大小,因為這減少了移植現有代碼所需的工作量。 - 結束說明

盡管不是必需的,但最好使原子的大小與其參數類型的大小相同。 實現此目的的方法是避免鎖定或將鎖存儲在單獨的結構中。 正如其他答案已經清楚解釋的那樣,哈希表用於保存所有鎖。 這是為使用中的所有原子對象存儲任意數量的鎖的最有效內存的方法。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM