簡體   English   中英

是什么讓boost :: shared_mutex如此緩慢

[英]What makes the boost::shared_mutex so slow

我使用谷歌基准測試運行以下3個測試,結果讓我感到驚訝,因為RW鎖定比釋放模式下的簡單互斥鎖慢約4倍。 (在調試模式下比簡單的互斥鎖慢~10倍)

void raw_access() {
    (void) (gp->a + gp->b);
}

void mutex_access() {
    std::lock_guard<std::mutex> guard(g_mutex);
    (void) (gp->a + gp->b);
}

void rw_mutex_access() {
    boost::shared_lock<boost::shared_mutex> l(g_rw_mutex);
    (void) (gp->a + gp->b);
}

結果是:

2019-06-26 08:30:45
Running ./perf
Run on (4 X 2500 MHz CPU s)
CPU Caches:
  L1 Data 32K (x2)
  L1 Instruction 32K (x2)
  L2 Unified 262K (x2)
  L3 Unified 4194K (x1)
Load Average: 5.35, 3.22, 2.57
-----------------------------------------------------------
Benchmark                 Time             CPU   Iterations
-----------------------------------------------------------
BM_RawAccess           1.01 ns         1.01 ns    681922241
BM_MutexAccess         18.2 ns         18.2 ns     38479510
BM_RWMutexAccess       92.8 ns         92.8 ns      7561437

我沒有通過谷歌獲得足夠的信息,所以希望在這里有所幫助。

謝謝

我不知道標准庫/ boost /等的細節。 實現有所不同,雖然看起來標准庫版本更快(恭喜,無論誰編寫它)

因此,我將嘗試在理論層面上解釋各種互斥體類型之間的速度差異,這將解釋為什么共享互斥體(應該)更慢。

原子旋轉鎖

更多 - 作為一個學術練習,考慮最簡單的線程安全“互斥式”實現:一個簡單的原子自旋鎖。

從本質上講,這只不過是std::atomic<bool>std::atomic_flag 它被初始化為false。 要“鎖定”互斥鎖,只需在循環中執行原子比較和交換操作,直到獲得錯誤值(即在將其原子設置為true之前,前一個值為false)。

std::atomic_flag flag = ATOMIC_FLAG_INIT;

// lock it by looping until we observe a false value
while (flag.test_and_set()) ;

// do stuff under "mutex" lock

// unlock by setting it back to false state
flag.clear();

但是,由於這個結構的性質,我們稱之為“不公平”的互斥鎖,因為獲取鎖的線程順序不一定是它們開始嘗試鎖定它的順序。 也就是說,在高爭用的情況下,線程可能會嘗試鎖定並且永遠不會成功,因為其他線程更幸運。 這是非常時間敏感的。 想象一下音樂椅。

因此,盡管它的功能類似於互斥體,但它並不是我們所認為的“互斥體”。

互斥

可以認為互斥鎖構建在原子自旋鎖之上(盡管它通常不是這樣實現的,因為它們通常是在支持操作系統和/或硬件的情況下實現的)。

從本質上講,互斥鎖是一個高於原子自旋鎖的步驟,因為它有一個等待線程的隊列。 這允許它“公平”,因為鎖獲取的順序(或多或少)與鎖定嘗試的順序相同。

如果您注意到,如果您運行sizeof(std::mutex)它可能會比您預期的要大一些。 在我的平台上它是40個字節。 該額外空間用於保存狀態信息,特別是包括訪問每個互斥鎖的隊列的某種方式。

當您嘗試鎖定互斥鎖時,它會執行一些低級線程安全操作,以便可以通過線程安全訪問互斥鎖的狀態信息(例如原子自旋鎖),檢查互斥鎖的狀態,將您的線程添加到鎖定隊列,並且(通常)在您等待時讓您的線程進入睡眠狀態,這樣您就不會浪費寶貴的CPU時間。 低級線程安全操作(例如原子自旋鎖)在線程進入休眠狀態的同時被原子釋放(這通常需要OS或硬件支持才能有效)。

解鎖是通過執行低級線程安全操作(例如原子旋轉鎖定),從隊列中彈出下一個等待線程並將其喚醒來執行的。 已被喚醒的線程現在“擁有”鎖。 沖洗並重復洗滌。

共享互斥鎖

共享互斥鎖使這一概念更進一步。 它可以由單個線程擁有讀/寫權限(如普通互斥),或者由多個線程擁有只讀權限(從語義上講,無論如何 - 由程序員來確保它是安全的)。

因此,除了唯一的所有權隊列(如普通的互斥鎖)之外,它還具有共享的所有權狀態。 共享所有權狀態可以簡單地計算當前具有共享所有權的線程數。 如果你檢查sizeof(std::shared_mutex)你會發現它通常比std::mutex更大。 例如,在我的系統上,它是56個字節。

所以當你去鎖定一個共享的互斥鎖時,它必須完成普通互斥鎖所做的一切,但還需要驗證其他一些東西。 例如,如果您嘗試唯一鎖定,則必須驗證沒有共享所有者。 當你試圖共享鎖定時,它必須驗證沒有唯一的所有者。

因為我們通常希望互斥鎖“公平”,所以一旦一個唯一的鎖定器在隊列中,未來的共享鎖定嘗試必須排隊而不是獲取鎖定,即使它當前可能被多個線程共享(即非唯一)鎖定。 這是為了確保共享所有者不“欺負”想要獨特所有權的線程。

但這也是另一種方式:排隊邏輯必須確保在共享所有權期間永遠不會將共享鎖定器置於空隊列中(因為它應立即成功並成為另一個共享所有者)。

此外,如果有一個獨特的儲物櫃,后面是一個共用的儲物櫃,后面是一個獨特的儲物櫃,它必須(大致)保證采購訂單。 因此,鎖定隊列中的每個條目也需要一個表示其目的的標志(即共享與唯一)。

然后我們想到了喚醒邏輯。 解鎖共享互斥鎖時,邏輯會根據互斥鎖的當前所有權類型而有所不同。 如果解鎖線程具有唯一所有權或者是最后一個共享所有者,則可能必須從隊列中喚醒某些線程。 它將喚醒隊列前面請求共享所有權的所有線程,或者喚醒隊列前面的單個線程請求唯一所有權。

正如您可以想象的那樣,所有這些額外的邏輯是由於什么原因而鎖定誰以及它如何根據當前所有者而不是根據隊列內容而變化使得這可能相當慢一些。 希望你閱讀頻率比你寫的頻率高得多,因此你可以讓許多共享所有者同時運行,這可以減輕協調所有這些的性能。

暫無
暫無

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

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