簡體   English   中英

x86上的原子計數器和自旋鎖的成本(_64)

[英]The cost of atomic counters and spinlocks on x86(_64)

前言

我最近遇到了一些同步問題,這使我成為自旋鎖原子計數器 然后我再搜索一下,這些如何工作並找到了std :: memory_order和內存屏障( mfencelfencesfence )。

所以現在,似乎我應該使用獲取/釋放螺旋鎖並放松計數器。

一些參考

x86 MFENCE - 記憶圍欄
x86 LOCK - 斷言LOCK#信號

這三個操作(lock = test_and_set ,unlock = clear ,increment = operator ++ = fetch_add )的機器代碼(編輯:見下文)什么 ,默認( seq_cst )內存順序和獲取/釋放/放松(按順序為那些三個操作)。 有什么區別 (哪些內存屏障在哪里) 和成本(多少CPU周期)?

目的

我只是想知道我的舊代碼(沒有指定內存順序=使用seq_cst)真的是多么糟糕,如果我應該創建一些派生自std::atomic但使用輕松內存排序的 class atomic_counter (以及帶有獲取/釋放的良好自旋鎖)某些地方的互斥體......或者使用boost庫中的東西 - 到目前為止我已經避免了提升

我的知識

到目前為止,我確實理解自旋鎖比自身保護更多(但也有一些共享資源/內存) ,因此,必須有一些東西可以使一些內存視圖對於多個線程/內核(即獲取/釋放和內存圍欄)保持一致) 原子計數器只為自己而存在,只需要原子增量(不涉及其他內存,當我讀它時我並不真正關心它的價值,它是信息性的,可以是幾個循環舊,沒問題) 有一些LOCK前綴和一些像xchg這樣的指令隱含了它。 在這里,我的知識結束了,我不知道緩存和總線是如何工作的以及背后的原因(但我知道現代CPU可以重新排序指令,並行執行它們並使用內存緩存和一些同步)。 謝謝你的解釋。

PS:我現在有舊的32位PC,只能看到lock addl和簡單的xchg ,沒有別的 - 所有版本看起來都一樣(除了解鎖),memory_order在我的舊PC上沒有區別(除了解鎖,發布使用move而不是xchg ) 。 64位PC會是這樣嗎? (編輯:見下文 )我是否需要關心記憶順序? (回答:不,不多,解鎖時釋放可以節省幾個周期,就是這樣。)

編碼:

#include <atomic>
using namespace std;

atomic_flag spinlock;
atomic<int> counter;

void inc1() {
    counter++;
}
void inc2() {
    counter.fetch_add(1, memory_order_relaxed);
}
void lock1() {
    while(spinlock.test_and_set()) ;
}
void lock2() {
    while(spinlock.test_and_set(memory_order_acquire)) ;
}
void unlock1() {
    spinlock.clear();
}
void unlock2() {
    spinlock.clear(memory_order_release);
}

int main() {
    inc1();
    inc2();
    lock1();
    unlock1();
    lock2();
    unlock2();
}

g ++ -std = c ++ 11 -O1 -S( 32位Cygwin ,縮短輸出)

__Z4inc1v:
__Z4inc2v:
    lock addl   $1, _counter    ; both seq_cst and relaxed
    ret
__Z5lock1v:
__Z5lock2v:
    movl    $1, %edx
L5:
    movl    %edx, %eax
    xchgb   _spinlock, %al      ; both seq_cst and acquire
    testb   %al, %al
    jne L5
    rep ret
__Z7unlock1v:
    movl    $0, %eax
    xchgb   _spinlock, %al      ; seq_cst
    ret
__Z7unlock2v:
    movb    $0, _spinlock       ; release
    ret

更新x86_64bit :(請參閱mfence中的unlock1

_Z4inc1v:
_Z4inc2v:
    lock addl   $1, counter(%rip)   ; both seq_cst and relaxed
    ret
_Z5lock1v:
_Z5lock2v:
    movl    $1, %edx
.L5:
    movl    %edx, %eax
    xchgb   spinlock(%rip), %al     ; both seq_cst and acquire
    testb   %al, %al
    jne .L5
    ret
_Z7unlock1v:
    movb    $0, spinlock(%rip)
    mfence                          ; seq_cst
    ret
_Z7unlock2v:
    movb    $0, spinlock(%rip)      ; release
    ret

x86主要是強大的內存模型 ,所有常用的存儲/加載都隱含了釋放/獲取語義。 唯一的例外是SSE非臨時存儲操作,需要像往常一樣對sfence進行排序。 具有LOCK前綴的所有讀 - 修改 - 寫(RMW)指令都意味着完全的內存屏障,即seq_cst。

因此在x86上,我們有

  • test_and_set可以與編碼lock bts (對於逐位操作), lock cmpxchg ,或lock xchg (或只是xchg這意味着lock )。 其他自旋鎖實現可以使用lock inc (或dec)等指令,如果它們需要例如公平性。 使用release / acquire fence實現try_lock是不可能的(至少你需要獨立的內存屏障mfence )。
  • clearlock and (用於按位)或lock xchg編碼,但更高效的實現將使用普通寫( mov )而不是鎖定指令。
  • fetch_add使用lock add編碼。

刪除lock前綴不能保證RMW操作的原子性,因此這些操作不能嚴格解釋為在C ++視圖中具有memory_order_relaxed 但是在實踐中,您可能希望在安全時(在構造函數中,在鎖定下)通過更快的非原子操作訪問原子變量。

根據我們的經驗,執行RMW原子操作究竟是什么並不重要,它們執行的循環次數幾乎相同(並且mfence約為鎖定操作的x0.5)。 您可以通過計算原子操作數(和mfences)以及內存間接數(緩存未命中數)來估計同步算法的性能。

我建議: x86-TSO:一個嚴格且可用的程序員x86多處理器模型

你的x86和x86_64確實非常“表現良好”。 特別是,它們不會重新排序寫操作(並且任何推測性寫入在它們位於cpu / core的寫入隊列中時都會被丟棄),並且它們不會重新排序讀取操作。 但是,它們將盡可能早地啟動讀取操作,這意味着可以重新讀取讀取和寫入操作 (讀取寫入隊列中的某些內容會讀取排隊的值,因此不會重新排序相同位置的讀取/寫入。)所以:

  • 讀 - 修改 - 寫操作需要LOCK ,這使得它們隱含地成為memory_order_seq_cst

    因此,對於這些操作,您無需通過削弱內存排序(在x86 / x86_64上)獲得任何收益。 一般的建議是“保持簡單”並堅持使用memory_order_seq_cst ,這對於x86和x86_64來說並不會花費額外的成本。

    對於比Pentium更新的任何東西,如果cpu / core已經對受影響的內存進行“獨占”訪問,則LOCK不會影響其他cpus / core,並且可能是一個相對簡單的操作。

  • memory_order_acquire / _release不需要mfence或任何其他開銷。

    因此,對於原子加載/存儲,如果獲取/釋放足夠,那么對於x86 / x86_64,這些操作是“免稅”的。

  • memory_order_seq_cst確實需要mfence ......

......這值得理解。

(注意:我們在這里談論處理器對編譯器生成的指令所做的事情。編譯器對操作的重新排序是一個非常類似的問題,但這里沒有解決。)

mfence停止cpu / core,直到所有掛起的寫入都從寫入隊列中清除。 特別是,在寫入隊列為空之前,任何跟隨mfence讀操作都不會啟動。 考慮兩個線程:

  initial state: wa = wb = 0

  thread 'A'                    thread 'B'
    wa = 1 ;  (mov [wa] ← 1)      wb = 1 ;   (mov [wb] ← 1)
    a  = wb ; (mov ebx ← [wb])    b  = wa ;  (mov ebx ← [wa])

留給自己的設備,x86 / x86_64可以產生任何(a = 1,b = 1),(a = 0,b = 1),(a = 1,b = 0) (a = 0,b) = 0)。 如果你期望memory_order_seq_cst,那么最后一個是無效的 - 因為你不能通過任何交錯操作得到它。 這可能發生的原因是wawb的寫入在相應的cpu / core的隊列中排隊,並且wawb的讀取都可以被調度,並且可以在寫入之前完成。 要實現memory_order_seq_cst,您需要一個mfence

  thread 'A'                    thread 'B'
    wa = 1 ;  (mov [wa] ← 1)      wb = 1 ;   (mov [wb] ← 1)
        mfence ;                      mfence
    a  = wb ; (mov ebx ← [wb])    b  = wa ;  (mov ebx ← [wa])

由於線程之間沒有同步,因此結果可能是 (a = 0,b = 0) 之外的任何內容。 有趣的是, mfence是為了線程本身的好處,因為它阻止了在寫完成之前開始的讀操作。 其他線程唯一關心的是寫入發生的順序,x86 / x86_64在任何情況下都不會重新排序。

因此,要實現memory_order_seq_cst atomic_load()atomic_store() ,必須在一個或多個存儲之后和加載之前插入一個mfence 在將這些操作實現為庫函數的情況下,常見的慣例是將mfence添加到所有商店,使負載“裸”。 (邏輯是負載比商店更常見,並且將開銷添加到商店似乎更好。)


至少對於自旋鎖,你的問題似乎歸結為旋轉解鎖操作是否需要一個mfence ,以及它有什么不同。

隱含地,C11 atomic_flag_clear()memory_order_seq_cst ,需要mfence C11 atomic_flag_test_and_set()不僅是一個讀 - 修改 - 寫操作,而且還隱含着memory_order_seq_cst - 而LOCK這樣做的。

C11在threads.h庫中沒有提供自旋鎖。 但是你可以使用atomic_flag - 雖然對於你的x86 / x86_64你有PAUSE指令問題需要處理。 問題是,你需要 memory_order_seq_cst嗎,特別是解鎖? 我認為答案是否定的 ,訣竅是: atomic_flag_test_and_set_explicit(xxx, memory_order_acquire)atomic_flag_clear(xxx, memory_order_release)

FWIW,glibc pthread_spin_unlock()沒有mfence gcc __sync_lock_release()也沒有(明確是“釋放”操作)。 但是gcc _atomic_clear()與C11 atomic_flag_clear()對齊,並采用內存順序參數。

mfence對解鎖有什么不同? 顯然,它對管道非常具有破壞性,並且由於沒有必要,因此根據具體情況確定其影響的確切規模並沒有多大成果。

spinlock不使用mfence,mfence只強制序列化/刷新當前核心的數據。 圍欄本身與原子操作無關。

對於自旋鎖,您需要某種原子動作來將數據交換到內存位置。 有許多不同的實現,針對不同的需求:例如,它是在內核還是用戶空間上工作? 是公平的嗎?

x86的一個非常簡單和愚蠢的自旋鎖看起來像這樣(我的內核使用它):

typedef volatile uint32_t _SPINLOCK __attribute__ ((aligned(16)));
static inline void _SPIN_LOCK(_SPINLOCK* lock) {
__asm (
       "cli\n"
       "lock bts %0, 0\n"
       "jnc 1f\n"
       "0:\n"
       "pause\n"
       "test %0, 1\n"
       "je 0b\n"
       "lock bts %0, 0\n"
       "jc 0b\n"
       "1:\n"
       :
       : "m"(lock)
       :
       );
}

邏輯很簡單

  1. 測試和交換一下,如果為零則意味着沒有采取鎖定,我們得到了它。
  2. 如果位不為零,則意味着鎖被其他人占用, pause是cpu制造商推薦的提示,因此它不會以緊密的外觀刻錄cpu。
  3. 循環,直到你得到鎖

注意1.您也可以使用內在函數和擴展來實現自旋鎖,它應該非常相似。

注意2. Spinlock不是通過循環來判斷的,一個理智的實現應該是相當快的,對於即時,上面的實現你應該首先嘗試在設計良好的用法中抓住鎖,如果沒有,修復算法或拆分鎖以防止/減少鎖爭用。

注3.您還應該考慮其他事項,如公平。

回覆

和成本(多少CPU周期)?

至少在x86上,執行內存同步(原子操作,防護)的指令具有非常可變的CPU周期延遲。 它們等待處理器存儲緩沖區刷新到內存,這會根據存儲緩沖區內容而發生很大變化。

例如,如果在將多個高速緩存行推出到主存儲器的memcpy()之后原子操作是直的,則延遲可能在100納秒內。 相同的原子操作,但在一系列僅寄存器算術指令之后,可能只需要幾個時鍾周期。

暫無
暫無

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

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