首页 > 其他分享 >关于 线程模型中经常使用的 __sync_fetch_and_add 原子操作的性能

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

时间:2022-11-04 11:32:53浏览次数:112  
标签:__ THREAD lock sync add STARTUPWORKER atomic


最近从 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;
#endif

uint64_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 ATOMIC
g_iSum ++;
#else
mu.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, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
xorl %eax, %eax
movl $0, -4(%rbp)
movl $1, -8(%rbp)
movl $1, %ecx
# lock前缀,这里是这个函数性能的关键。
lock xaddl %ecx, -8(%rbp)
movl %ecx, -12(%rbp)
popq %rbp
retq
.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​​ 前缀 锁内存总线来说效率高多了。


标签:__,THREAD,lock,sync,add,STARTUPWORKER,atomic
From: https://blog.51cto.com/u_13456560/5823176

相关文章

  • vim 键盘宏操作 -- 大道至简
    最近利用vim做一些文本处理时发现vim支持的键盘宏是一个好东西啊,高效优雅得处理大量需要重复性操作的文本,让人爱不释手!!!希望接下来对键盘宏的分享能够实际帮助到大家。后文......
  • 存储引擎 K/V 分离下的index回写问题
    前言近期在做onnvmehash引擎相关的事情,对于非全序的数据集的存储需求,相比于我们传统的LSM或者B-tree的数据结构来说能够减少很多维护全序上的计算/存储资源。当然我们要......
  • C++智能指针:weak_ptr实现详解
    文章目录​​weak_ptr描述​​​​声明​​​​作用​​​​原理实现​​​​函数成员使用​​​​总结​​weak_ptr描述声明头文件:​​<memory>​​​模版类:​​template......
  • Rocksdb 的内存分配器--ConcurrentArena 实现原理
    文章目录​​RocksdbConcurrentArena实现原理​​​​基本架构​​​​内存分配过程​​​​内存释放过程​​​​ConcurrentArena分配器和其他内存分配器区别和联系​......
  • Rocksdb 日志分析工具 -- 性能和稳定性分析
    文章目录​​1.前言​​​​2.工具使用细节​​​​3.如何制作一个自己的python-package​​​​3.1项目配置文件​​​​3.1.1LICENSE文件​​​​3.2README.md文......
  • WCNSS_qcom_cfg.ini WIFI配置文件解析
    STA相关的一般配置gChannelBondingMode5GHz=1gChannelBondingMode24GHz=0//通道绑定gStaKeepAlivePeriod=30//使用非零周期值启用保持活动状态gVhtMpduLen=2......
  • 蚂蚁集团绿色大赛---赛队招募啦!!
    2022年度大赛——蚂蚁集团绿色计算大赛,正式开赛!蚂蚁集团联合绿色计算产业联盟,面向广大学生和科技从业者发起实战精英挑战赛!规模宏大、阵容豪华、百万奖池、在线挑战,诚邀......
  • Python内置模块FTPLIb的 STORLINES方法运行出错解决过程
    FTPLIbSTORLINES方法出错解决过程错误现象importftplibimportoptparseimportsysclassFTPWebpageDetect:def__init__(self)->None:self.target......
  • ArcGIS JS API 添加要素图层 点击时获取图层属性
    //需要引入:"esri/layers/FeatureLayer"模块//要素图层被点击时弹出图层属性的模板定义{为字段}varTuCeng03TC={"title":"ID:{objectid}",......
  • curl 获取响应的状态码
    需要在执行curl_exec后再通过curl_getinfo来获取。$ch=curl_init();curl_setopt($ch,CURLOPT_URL,'http://www.google.com.hk');curl_setopt($ch,CURLOPT_TIMEOUT......