关于 线程模型中经常使用的 __sync_fetch_and_add 原子操作的性能
最近從 kvell 這篇論文中看到一些單機(jī)存儲(chǔ)引擎的優(yōu)秀設(shè)計(jì),底層存儲(chǔ)硬件性能在不遠(yuǎn)的未來可能不再是主要的性能瓶頸,反而高并發(fā)下的CPU可能是軟件性能的主要限制。像BPS/AEP/Optane-SSD 等Intel 推出的硬件存儲(chǔ)棧已經(jīng)能夠在延時(shí)上接近DRAM的量級(jí),吞吐在較低的隊(duì)列深度下更是能夠超越當(dāng)前主流NVMe-ssd 數(shù)倍甚至一個(gè)量級(jí);同時(shí)結(jié)合 SPDK/io_uring/ZNS 等新型底層軟件棧,更是能夠在操作系統(tǒng)層級(jí)完全發(fā)揮硬件性能。
這個(gè)時(shí)候,我們的軟件設(shè)計(jì)模型需要適配硬件的發(fā)展。這里KVell 提出的單機(jī)引擎軟件棧就是 shard-nothing。 即引擎層調(diào)度I/O的時(shí)候是單線程的,每個(gè)調(diào)度線程綁定一個(gè) CPU-core 來完整調(diào)度整個(gè)IO的處理,這個(gè)調(diào)度方式的優(yōu)劣會(huì)在后面介紹KVell 的時(shí)候詳細(xì)描述(NUMA架構(gòu)下對(duì)cpu的訪存非常友好)。總之,多線程獨(dú)立處理請(qǐng)求的模型 也是 ceph 最新的 crimson osd 正在進(jìn)行重構(gòu)的主體架構(gòu)。
本文主要討論的是在 KVell 的實(shí)現(xiàn)中看到 一個(gè)線程間同步數(shù)據(jù)的調(diào)度函數(shù)的使用__sync_fetch_and_add,它是GCC 提供的針對(duì)一個(gè)變量的原子操作。
有點(diǎn)好奇為什么KVell 會(huì)使用這個(gè)函數(shù)原子操作這個(gè)變量,按照我們的編程習(xí)慣,我們?yōu)榱朔乐苟嗑€程對(duì)同一個(gè)變量的修改不是原子的,可能會(huì)考慮使用排他鎖或者內(nèi)核的 atomic_* 系列操作,但是它這里使用了這個(gè)函數(shù),那這個(gè)選擇肯定是有原因的(當(dāng)然可能C語言沒有atomic 庫,所以沒法直接用)。
所以就簡(jiǎn)單做了一個(gè)性能測(cè)試:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <mutex>
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
#include <sys/time.h>int g_iFlagAtom = 1;
#define WORK_SIZE 5000000
#define WORKER_COUNT 10
std::vector<std::thread> g_tWorkerID;
std::mutex mu;
#ifdef ATOMIC
std::atomic<int> g_iSum;
#else
int g_iSum = 0;
#endifuint64_t NowMicros() {struct timeval tv;gettimeofday(&tv, nullptr);return static_cast<uint64_t>(tv.tv_sec) * 1000000 + tv.tv_usec;
}void * thr_worker(int tid) {printf ("WORKER THREAD %d STARTUP\n", tid);int i=0;for (i=0; i<WORK_SIZE; ++i) {if (g_iFlagAtom) {
#ifdef ATOMIC
#else__sync_fetch_and_add(&g_iSum, 1);
#endif} else {
#ifdef ATOMICg_iSum ++;
#elsemu.lock();g_iSum ++;mu.unlock();
#endif}}return NULL;
}int main(int argc, char* argv[]) {if (argc < 2) {printf("args < 2");return -1;}g_iFlagAtom = atoi(argv[1]);int i;for (i=0;i<WORKER_COUNT;++i) {g_tWorkerID.push_back(std::thread(thr_worker, i));}uint64_t start = NowMicros();for (i=0;i<g_tWorkerID.size();++i) {g_tWorkerID[i].join();}printf ("CREATED %d WORKER THREADS\n", i);std::cout << "THE SUM :" << g_iSum << " TIME:" << NowMicros() - start<< "us" << std::endl;return 0;
}
- 對(duì)比
__sync_fetch_and_add和普通排他鎖之間的性能差異:g++ -std=c++11 test_atomic.cc -o test_atomic# 普通鎖 性能$ ./test_atomic 0 WORKER THREAD 1 STARTUP WORKER THREAD 0 STARTUP WORKER THREAD 2 STARTUP WORKER THREAD 3 STARTUP WORKER THREAD 5 STARTUP WORKER THREAD 4 STARTUP WORKER THREAD 7 STARTUP WORKER THREAD 6 STARTUP WORKER THREAD 8 STARTUP WORKER THREAD 9 STARTUP CREATED 10 WORKER THREADS THE SUM :50000000 TIME:2870679us # __sync_fetch_and_add 性能$ ./test_atomic 1 WORKER THREAD 0 STARTUP WORKER THREAD 1 STARTUP WORKER THREAD 3 STARTUP WORKER THREAD 4 STARTUP WORKER THREAD 2 STARTUP WORKER THREAD 5 STARTUP WORKER THREAD 6 STARTUP WORKER THREAD 7 STARTUP WORKER THREAD 8 STARTUP WORKER THREAD 9 STARTUP CREATED 10 WORKER THREADS THE SUM :50000000 TIME:1138828us - 對(duì)比
atomic原子變量的性能:g++ -std=c++11 test_atomic.cc -o test_atomic -DATOMIC$ ./test_atomic 0 WORKER THREAD 0 STARTUP WORKER THREAD 5 STARTUP WORKER THREAD 6 STARTUP WORKER THREAD 3 STARTUP WORKER THREAD 4 STARTUP WORKER THREAD 9 STARTUP WORKER THREAD 2 STARTUP WORKER THREAD 1 STARTUP WORKER THREAD 7 STARTUP WORKER THREAD 8 STARTUP CREATED 10 WORKER THREADS THE SUM :50000000 TIME:1180191us
從上面的測(cè)試數(shù)據(jù)可以整體看到__sync_fetch_and_add 的性能是比互斥鎖性能好數(shù)倍,而和atomic的性能差不多。
為什么__sync_fetch_and_add 性能比互斥鎖好呢?
我們來看一下如下代碼的匯編實(shí)現(xiàn)。
#include <iostream>
int main() {int a;__sync_fetch_and_add(&a, 1);return 0;
}
編譯: g++ -S test_sync_fetch_and_add.cc -o t.s
.section __TEXT,__text,regular,pure_instructions.build_version macos, 11, 0 sdk_version 11, 1.globl _main ## -- Begin function main.p2align 4, 0x90
_main: ## @main.cfi_startproc
## %bb.0:pushq %rbp.cfi_def_cfa_offset 16.cfi_offset %rbp, -16movq %rsp, %rbp.cfi_def_cfa_register %rbpxorl %eax, %eaxmovl $0, -4(%rbp)movl $1, -8(%rbp)movl $1, %ecx# lock前綴,這里是這個(gè)函數(shù)性能的關(guān)鍵。lock xaddl %ecx, -8(%rbp)movl %ecx, -12(%rbp)popq %rbpretq.cfi_endproc## -- End function
.subsections_via_symbols
其中匯編代碼中有一個(gè)lock 前綴, 這個(gè)lock 前綴后面跟的是一個(gè)xaddl的指令。
這里萬分感謝一位同事對(duì)lock前綴實(shí)現(xiàn)上的指正,現(xiàn)如今博客上的內(nèi)容很多都是幾年前甚至十幾年前的技術(shù),如今隨著硬件的高速發(fā)展,這一些信息如果不能及時(shí)跟進(jìn)最新的技術(shù)動(dòng)態(tài),往往會(huì)誤導(dǎo)后續(xù)學(xué)習(xí)的同學(xué),在最新技術(shù)迭代方面,以后一定會(huì)持續(xù)求證,保證總結(jié)的信息是準(zhǔn)確的,并且后續(xù)持續(xù)更新之前的一些技術(shù)博客,以防誤導(dǎo)他人。
關(guān)于lock前綴的實(shí)現(xiàn),在 Intel486 和 Pentium processors 以及之前的處理器上面確實(shí)會(huì)在指令執(zhí)行期間對(duì)內(nèi)存總線進(jìn)行加鎖。
但是在 intel P6 和 更新的處理器上面,鎖前綴已經(jīng)不再是對(duì)內(nèi)存總線進(jìn)行加鎖了,而是通過緩存一致性原理加鎖當(dāng)前處理器的cache,即當(dāng)前的cpu-cache 所訪問的內(nèi)存,防止其他的cpu訪問或者修改當(dāng)前的cpu-cache中對(duì)應(yīng)內(nèi)存的內(nèi)容,這樣的加鎖粒度更小,也更高效。更細(xì)節(jié)的內(nèi)容可以參考intel 官方文檔 中的8.1.4部分。
下面是原回答:
在x86 平臺(tái)上, CPU 提供了指令執(zhí)行期間加鎖內(nèi)存總線的手段。也就是通過這個(gè)
lock前綴,標(biāo)識(shí)后續(xù)的一個(gè)指令的執(zhí)行之前會(huì)加鎖內(nèi)存總線,而同處于當(dāng)前內(nèi)存總線的其他CPU的指令在此期間無法修改內(nèi)存,等到lock 后面的一個(gè)指令執(zhí)行完畢才會(huì)釋放內(nèi)存總線的鎖。用這個(gè)指令前綴能夠?qū)崿F(xiàn) CAS 以及 spinlock。
以下是__sync_fetch_and_add 函數(shù)的簡(jiǎn)單實(shí)現(xiàn):
inline unsigned int __sync_fetch_and_sub(volatile unsigned int* p,unsigned int decr)
{unsigned int result;__asm__ __volatile__ ("lock; xadd %0, %1":"=r"(result), "=m"(*p):"0"(-decr), "m"(*p):"memory");return result;
}
同樣的,我們?cè)贑++的 標(biāo)準(zhǔn)庫的 atomic 代碼的匯編中 中也能看到帶有l(wèi)ock 前綴的指令。那這種 lock 前綴加鎖內(nèi)存總線相比于我們的排他鎖的實(shí)現(xiàn)的性能差異體現(xiàn)在哪呢?
mu.lock() 或者 pthread_mutex_lock() 底層都是會(huì)調(diào)用操作系統(tǒng)的futex 系統(tǒng)調(diào)用,這個(gè)是操作系統(tǒng)層級(jí)調(diào)度線程的原子操作時(shí)的調(diào)度方式。它的粒度是操作系統(tǒng)級(jí)別的,讓其他想要訪問當(dāng)前內(nèi)存地址的線程掛起,等待增在訪問內(nèi)存的線程執(zhí)行完畢再調(diào)度其他的線程。這樣的調(diào)度粒度往往是線程的大量上下文信息在CPU cache中的load 和 overload。相比于__sync_fetch_and_sub 函數(shù)中的lock 前綴 鎖內(nèi)存總線來說效率高多了。
總結(jié)
以上是生活随笔為你收集整理的关于 线程模型中经常使用的 __sync_fetch_and_add 原子操作的性能的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 50平米装修多少钱啊?
- 下一篇: KVell 单机k/v引擎:用最少的CP