[英]Relative performance of std::atomic and std::mutex
我正在考慮為項目實施隊列的選項,其要求是生產者至少必須盡可能低延遲。 為此,我一直在研究使用std::atomic
來控制生產者和消費者線程對數據結構的訪問的“無鎖”隊列。 我希望這可以避免std::mutex
的開銷,特別是代碼當前使用的std::unique_lock
。
為此,我編寫了一個簡單的測試程序來評估std::mutex
(加上std::unique_lock
)和std::atomic
的相對性能。 該程序還進行檢查以確保原子 object 是無鎖的,它是。
#include <mutex>
#include <atomic>
#include <thread>
#include <chrono>
#include <iostream>
#define CYCLES 100000000
void testAtomic()
{
bool var(true);
std::atomic_bool _value(true);
std::cout << "atomic bool is ";
if(!_value.is_lock_free())
std::cout << "not ";
std::cout << "lock free" << std::endl;
const auto _start_time = std::chrono::high_resolution_clock::now();
for(size_t counter = 0; counter < CYCLES; counter++)
{
var = _value.load();
var = !var;
_value.store(var);
}
const auto _end_time = std::chrono::high_resolution_clock::now();
std::cout << 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>
(_end_time - _start_time).count() << " s" << std::endl;
}
void testMutex()
{
bool var(true);
std::mutex _mutex;
std::chrono::high_resolution_clock _clock;
const auto _start_time = std::chrono::high_resolution_clock::now();
for(size_t counter = 0; counter < CYCLES; counter++)
{
std::unique_lock<std::mutex> lock(_mutex);
var = !var;
}
const auto _end_time = std::chrono::high_resolution_clock::now();
std::cout << 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>
(_end_time - _start_time).count() << " s" << std::endl;
}
int main()
{
std::thread t1(testAtomic);
t1.join();
std::thread t2(testMutex);
t2.join();
return 0;
}
運行此程序時,我得到以下 output:
atomic bool is lock free
3.49434 s
2.31755 s
這向我表明std::mutex
(和std::unique_lock
)要快得多,這與我閱讀有關原子與互斥鎖的期望相反。 我的發現正確嗎? 我的測試程序有問題嗎? 我對兩者之間差異的理解不正確嗎?
代碼在 CentOS7 上使用 GCC 4.8.5 編譯
你在什么硬件上測試?
由於您使用的是 GCC,因此std::atomic
seq_cst 存儲將使用mov
+ slow mfence
而不是慢一些的xchg
-with-mem (這也是一個完整的障礙,就像所有其他 x86 原子 RMW 操作一樣)。
采用互斥體需要原子 RMW(如xchg
,而不是 mov + mfence)。 如果你幸運地釋放,互斥鎖可能只是一個普通的存儲(比如mo_release
)。 競爭為零,因此獲取鎖總是成功的。
顯然,這些互斥鎖/解鎖庫函數背后的代碼比mfence
,尤其是在具有更新微碼的 Skylake CPU 上,其中mfence
是亂序執行的完整障礙以及 memory 。 (請參閱此答案的底部,以及lock xchg 是否具有與 mfence 相同的行為? )
另外,請注意,您的互斥循環將本地bool var
優化為寄存器,實際上並沒有在循環內的 memory 中更新它。 (您在 Godbolt 編譯器資源管理器中使用 gcc4.8.5 的代碼)。
# the main loop from testMutex
.L80: # do {
mov rdi, rsp # pointer to _mutex on the stack
call __gthrw_pthread_mutex_lock(pthread_mutex_t*)
test eax, eax
jne .L91 # mutex error handling
mov rdi, rsp # pointer to _mutex again
call __gthrw_pthread_mutex_unlock(pthread_mutex_t*)
sub rbx, 1
jne .L80 # }while(--counter)
循環內的xor bl, 1
將無關緊要; 亂序執行可能與其他工作重疊。
If a reference to var
escaped the function so the compiler had to have it in sync in memory before non-inline function calls (including to pthread library functions), we'd expect something like xor byte ptr [rsp+8], 1
. 這也將非常便宜,並且可能大部分被亂序執行隱藏,盡管加載/ALU/存儲可能是在耗盡存儲緩沖區時必須等待完整屏障的東西。
std::atomic
代碼:您似乎有意避免執行原子 RMW,而是加載到 tmp var 並執行單獨的存儲。 如果你只使用 release 而不是 seq_cst,那么它就可以編譯為 x86 上的普通存儲指令。 (或對大多數其他 ISA 設置更便宜的壁壘)。
bool tmp = _value.load(std::memory_order_relaxed); // or acquire
_value.store(!tmp, std::memory_order_release);
這應該以每個反轉大約 6 個周期運行,只是一個 ALU 操作的延遲加上存儲/重新加載的存儲轉發延遲。 而對於mfence
( https://uops.info/ )的最佳吞吐量,每次迭代可能需要 33 個周期。
或者由於這是一個非原子修改,只需存儲交替值而不重新讀取舊值。 通常只能在只有一個生產者寫入值而其他線程正在讀取的情況下才能避免原子 RMW。 因此,讓生產者將其正在修改的值保存在寄存器(非原子本地變量)中,如果存在則存儲副本。
bool var = true;
for(size_t counter = 0; counter < CYCLES; counter++)
{
var = !var;
_value.store(var, std::memory_order_release);
}
此外,不要在您自己的 var 名稱中使用前導下划線。 這些名稱是為實現而保留的。 (帶小寫的單個_
僅保留在文件/全局 scope 中,但這仍然是不好的做法。)
有人告訴我,最初在互斥鎖的某些實現中,它會首先在內部自旋鎖。
這可能就是它看起來更快的原因。
如果進行系統調用,我懷疑你會得到相同的結果。
(我無法驗證這一點,但我認為這可能是一個原因)
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.