IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 关于 线程模型中经常使用的 __sync_fetch_and_add 原子操作的性能 -> 正文阅读

[C++知识库]关于 线程模型中经常使用的 __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;
#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的指令。在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 前缀 锁内存总线来说效率高多了。

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-08-01 14:19:22  更:2021-08-01 14:20:05 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/1 11:40:11-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码