日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 人文社科 > 生活经验 >内容正文

生活经验

关于 线程模型中经常使用的 __sync_fetch_and_add 原子操作的性能

發布時間:2023/11/27 生活经验 43 豆豆
生活随笔 收集整理的這篇文章主要介紹了 关于 线程模型中经常使用的 __sync_fetch_and_add 原子操作的性能 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

最近從 kvell 這篇論文中看到一些單機存儲引擎的優秀設計,底層存儲硬件性能在不遠的未來可能不再是主要的性能瓶頸,反而高并發下的CPU可能是軟件性能的主要限制。像BPS/AEP/Optane-SSD 等Intel 推出的硬件存儲棧已經能夠在延時上接近DRAM的量級,吞吐在較低的隊列深度下更是能夠超越當前主流NVMe-ssd 數倍甚至一個量級;同時結合 SPDK/io_uring/ZNS 等新型底層軟件棧,更是能夠在操作系統層級完全發揮硬件性能。

這個時候,我們的軟件設計模型需要適配硬件的發展。這里KVell 提出的單機引擎軟件棧就是 shard-nothing。 即引擎層調度I/O的時候是單線程的,每個調度線程綁定一個 CPU-core 來完整調度整個IO的處理,這個調度方式的優劣會在后面介紹KVell 的時候詳細描述(NUMA架構下對cpu的訪存非常友好)。總之,多線程獨立處理請求的模型 也是 ceph 最新的 crimson osd 正在進行重構的主體架構。

本文主要討論的是在 KVell 的實現中看到 一個線程間同步數據的調度函數的使用__sync_fetch_and_add,它是GCC 提供的針對一個變量的原子操作。

有點好奇為什么KVell 會使用這個函數原子操作這個變量,按照我們的編程習慣,我們為了防止多線程對同一個變量的修改不是原子的,可能會考慮使用排他鎖或者內核的 atomic_* 系列操作,但是它這里使用了這個函數,那這個選擇肯定是有原因的(當然可能C語言沒有atomic 庫,所以沒法直接用)。

所以就簡單做了一個性能測試:

#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;
}
  • 對比 __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
    
  • 對比 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
    

從上面的測試數據可以整體看到__sync_fetch_and_add 的性能是比互斥鎖性能好數倍,而和atomic的性能差不多。

為什么__sync_fetch_and_add 性能比互斥鎖好呢?

我們來看一下如下代碼的匯編實現。

#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前綴,這里是這個函數性能的關鍵。lock		xaddl	%ecx, -8(%rbp)movl	%ecx, -12(%rbp)popq	%rbpretq.cfi_endproc## -- End function
.subsections_via_symbols

其中匯編代碼中有一個lock 前綴, 這個lock 前綴后面跟的是一個xaddl的指令。
這里萬分感謝一位同事對lock前綴實現上的指正,現如今博客上的內容很多都是幾年前甚至十幾年前的技術,如今隨著硬件的高速發展,這一些信息如果不能及時跟進最新的技術動態,往往會誤導后續學習的同學,在最新技術迭代方面,以后一定會持續求證,保證總結的信息是準確的,并且后續持續更新之前的一些技術博客,以防誤導他人。

關于lock前綴的實現,在 Intel486 和 Pentium processors 以及之前的處理器上面確實會在指令執行期間對內存總線進行加鎖。
但是在 intel P6 和 更新的處理器上面,鎖前綴已經不再是對內存總線進行加鎖了,而是通過緩存一致性原理加鎖當前處理器的cache,即當前的cpu-cache 所訪問的內存,防止其他的cpu訪問或者修改當前的cpu-cache中對應內存的內容,這樣的加鎖粒度更小,也更高效。更細節的內容可以參考intel 官方文檔 中的8.1.4部分。

下面是原回答:

在x86 平臺上, CPU 提供了指令執行期間加鎖內存總線的手段。也就是通過這個lock前綴,標識后續的一個指令的執行之前會加鎖內存總線,而同處于當前內存總線的其他CPU的指令在此期間無法修改內存,等到lock 后面的一個指令執行完畢才會釋放內存總線的鎖。用這個指令前綴能夠實現 CAS 以及 spinlock。

以下是__sync_fetch_and_add 函數的簡單實現:

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++的 標準庫的 atomic 代碼的匯編中 中也能看到帶有lock 前綴的指令。那這種 lock 前綴加鎖內存總線相比于我們的排他鎖的實現的性能差異體現在哪呢?
mu.lock() 或者 pthread_mutex_lock() 底層都是會調用操作系統的futex 系統調用,這個是操作系統層級調度線程的原子操作時的調度方式。它的粒度是操作系統級別的,讓其他想要訪問當前內存地址的線程掛起,等待增在訪問內存的線程執行完畢再調度其他的線程。這樣的調度粒度往往是線程的大量上下文信息在CPU cache中的load 和 overload。相比于__sync_fetch_and_sub 函數中的lock 前綴 鎖內存總線來說效率高多了。

總結

以上是生活随笔為你收集整理的关于 线程模型中经常使用的 __sync_fetch_and_add 原子操作的性能的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。