CPU Cache对于并发编程的影响

文章目录

  • 引子
  • CPU Cache对于并发的影响
  • 读写顺序对性能的影响
  • 字节对齐对Cache的影响
  • 小结

引子

下面给出两个极其相似的代码,运行出的时间却是有很大差别:
代码一

#include <stdio.h>
#include <pthread.h>
#include <stdint.h>
#include <assert.h>
#include<chrono>const uint32_t MAX_THREADS = 16;void* ThreadFunc(void* pArg)
{for (int i = 0; i < 1000000000; ++i) // 10亿次累加操作{++*(uint64_t*)pArg;}return NULL;
}int main() {static uint64_t aulArr[MAX_THREADS * 8];pthread_t aulThreadID[MAX_THREADS];auto begin = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch());for (int i = 0; i < MAX_THREADS; ++i){assert(0 == pthread_create(&aulThreadID[i], nullptr, ThreadFunc, &aulArr[i]));}for (int i = 0; i < MAX_THREADS; ++i){assert(0 == pthread_join(aulThreadID[i], nullptr));}auto end = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch());printf("%lld",end.count() - begin.count());
}

耗时: 26396ms

代码二

#include <stdio.h>
#include <pthread.h>
#include <stdint.h>
#include <assert.h>
#include<chrono>const uint32_t MAX_THREADS = 16;void* ThreadFunc(void* pArg)
{for (int i = 0; i < 1000000000; ++i) // 10亿次累加操作{++*(uint64_t*)pArg;}return NULL;
}int main() {static uint64_t aulArr[MAX_THREADS * 8];pthread_t aulThreadID[MAX_THREADS];auto begin = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch());for (int i = 0; i < MAX_THREADS; ++i){assert(0 == pthread_create(&aulThreadID[i], nullptr, ThreadFunc, &aulArr[i * 8]));}for (int i = 0; i < MAX_THREADS; ++i){assert(0 == pthread_join(aulThreadID[i], nullptr));}auto end = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch());printf("%lld",end.count() - begin.count());
}

耗时: 6762ms

这两者的主要差别就在于pthread_create传入的一个是aulArr[i]一个是aulArr[i * 8]

CPU Cache对于并发的影响

cpu cache在做数据同步的时候,有个最小的单位:cache line,当前主流CPU为64字节。
多个CPU读写相同的Cache line的时候需要做一致性同步,多CPU访问相同的Cache Line地址,数据会被反复写脏,频繁进行一致性同步。当多CPU访问不同的Cache Line地址时,无需一致性同步。
在上面的程序中:
static uint64_t aulArr[MAX_THREADS * 8];
占用的数据长度为:8byte * 8 * 16;
8byte * 8=64byte
程序一,每个线程在当前CPU读取数据时,访问的是同一块cache line
程序二,每个线程在当前CPU读取数据时,访问的是不同块的cache line,避免了对一个流水线的反复擦写,效率直线提升。
在这里插入图片描述

读写顺序对性能的影响

CPU会有一个预读,顺带着将需要的块儿旁边的块儿一起读出来放到cache中。所以当我们顺序读的时候就不需要从内存里面读了,可以直接在缓存里面读。
顺序读

#include <stdio.h>
#include <stdint.h>
#include <assert.h>
#include<chrono>
#include "string.h"int main() {const uint32_t BLOCK_SIZE = 8 << 20;// 64字节地址对齐,保证每一块正好是一个CacheLinestatic char memory[BLOCK_SIZE][64] __attribute__((aligned(64)));assert((uint64_t)memory % 64 == 0);memset(memory, 0x3c, sizeof(memory));int n = 10;auto begin = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch());while (n--){char result = 0;for (int i = 0; i < BLOCK_SIZE; ++i){for (int j = 0; j < 64; ++j){result ^= memory[i][j];}}}auto end = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch());printf("%lld",end.count() - begin.count());
}

乱序读

#include <stdio.h>
#include <stdint.h>
#include <assert.h>
#include<chrono>
#include "string.h"int main() {const uint32_t BLOCK_SIZE = 8 << 20;// 64字节地址对齐,保证每一块正好是一个CacheLinestatic char memory[BLOCK_SIZE][64] __attribute__((aligned(64)));assert((uint64_t)memory % 64 == 0);memset(memory, 0x3c, sizeof(memory));int n = 10;auto begin = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch());while (n--){char result = 0;for (int i = 0; i < BLOCK_SIZE; ++i){int k = i * 5183 % BLOCK_SIZE;  // 人为打乱顺序for (int j = 0; j < 64; ++j){result ^= memory[k][j];}}}auto end = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch());printf("%lld",end.count() - begin.count());
}

顺序读耗时13547ms,随机乱序读耗时21395ms。
如果一定要随机读的话该怎么优化呢?
如果我们知道我们下一轮读取的数据,并且不是要立即访问这个地址的话,使用_mm_prefetch指令优化,告诉CPU提前预读下一轮循环的cacheline
有关该指令可以参考官方文档:https://docs.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2010/84szxsww(v=vs.100)
使用该命令后,再看看运行时间:

#include <stdio.h>
#include <stdint.h>
#include <assert.h>
#include<chrono>
#include "string.h"
#include "xmmintrin.h"int main() {const uint32_t BLOCK_SIZE = 8 << 20;// 64字节地址对齐,保证每一块正好是一个CacheLinestatic char memory[BLOCK_SIZE][64] __attribute__((aligned(64)));assert((uint64_t)memory % 64 == 0);memset(memory, 0x3c, sizeof(memory));int n = 10;auto begin = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch());while (n--){char result = 0;for (int i = 0; i < BLOCK_SIZE; ++i){int next_k = (i + 1) * 5183 % BLOCK_SIZE;_mm_prefetch(&memory[next_k][0], _MM_HINT_T0);int k = i * 5183 % BLOCK_SIZE;  // 人为打乱顺序for (int j = 0; j < 64; ++j){result ^= memory[k][j];}}}auto end = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch());printf("%lld",end.count() - begin.count());
}

从原来的21395ms优化到15291ms

字节对齐对Cache的影响

在2GB内存,int64为单元进行26亿次异或。分别测试地址对齐与非对齐 在顺序访问和随机访问下的耗时

非地址对齐地址对齐耗时比
顺序访问7.8s7.7s1.01:1
随机访问90s80s1.125:1

在顺序访问时,Cache命中率高,且CPU预读,此时差别不大。
在随机访问的情况下,Cache命中率几乎为0,有1/8概率横跨2个cacheline,此时需读两次内存,此时耗时比大概为:7 / 8 * 1 + 1 / 8 * 2 = 1.125
结论就是:
1、cacheline 内部访问非字节对齐变量差别不大
2、跨cacheline访问代价主要为额外的内存读取开销
所以除了网络协议以外,避免出现1字节对齐的情况。可以通过调整成员顺序,减少内存开销。

小结

1、多线程尽量避免读写相同的cache line内存
2、线程访问对象尽可能与cacheline地址对齐
3、尽可能对内存做顺序读写,否则可使用CPU预读指令
4、变量保持地址对齐

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/376670.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

软工之初识

我们之前已经在完全不懂软件工程的情况下&#xff0c;已经做完了两个小系统&#xff0c;虽然能够运行&#xff0c;但其中有很多的问题&#xff0c;学习软工就是让我们在工程学原理的指导之下去开发和设计软件。 软件工程同大多数书讲的都是一样的&#xff0c;首先对软件工程有一…

perf +火焰图使用

以mysqld进程为例&#xff1a; [rootVM-90-225-centos ~]# ps -ef | grep mysqld root 9808 9621 0 19:30 pts/7 00:00:00 grep --colorauto mysqld root 16104 1 0 17:30 pts/0 00:00:00 /bin/sh /usr/local/mysql/bin/mysqld_safe --datadir/usr/loc…

在一个字符串中找到第一个只出现一次的字符

题目&#xff1a;在一个字符串中找到第一个只出现一次的字符&#xff0c;如输入abaccdeff&#xff0c;则输出b&#xff1b;具体实现如下&#xff1a;#include <iostream> #include <string> using namespace std; void FindChar(const string &strBuf) {int nA…

零拷贝机制在文件传输中的使用手法

文章目录文件传输&#xff08;读取与发送&#xff09;中的拷贝与上下文切换零拷贝技术sendfilesendfile SG-DMAmmap writespliceDirect I/O经典应用文件传输&#xff08;读取与发送&#xff09;中的拷贝与上下文切换 如果服务端要提供文件传输的功能&#xff0c;最简单的方式…

POJ 3233 Matrix Power Series 矩阵快速幂 + 二分

题意&#xff1a;求矩阵的次方和 解题思路&#xff1a;最容易想到方法就是两次二分因为 我们可以把一段 A^1 A^2 .......A^K 变成 A^1 ..A^(K/2) ( A^1 ..A^(K/2))*(A^(k/2)) 当k 为奇数的时候 或者 A^1 ..A^(K/2) ( A^1 ..A^(K/2))*(A^(k/2)) A^K 当K 为偶数的时候…

时间序列进行分析的一些手法以及代码实现(移动平均、指数平滑、SARIMA模型、时间序列的(非)线性模型)

文章目录1、移动平均moving average方法weighted average方法2、指数平滑单指数平滑 exponential_smoothing双指数平滑三指数平滑 Triple exponential smoothing3、平稳性以及时间序列建模SARIMA模型4、时间序列的&#xff08;非&#xff09;线性模型时间序列的滞后值使用线性回…

三大平衡树(Treap + Splay + SBT)总结+模板

Treap树 核心是 利用随机数的二叉排序树的各种操作复杂度平均为O(lgn) Treap模板&#xff1a; #include <cstdio> #include <cstring> #include <ctime> #include <iostream> #include <algorithm> #include <cstdlib> #include <cmath…

mysqld进程 ut_delay 占用率过高

采用性能分析工具perf top -p mysqld进程 在测试mysql数据库时&#xff0c;用perf top如果看到热点函数是ut_delay或者_raw_spin_lock的话&#xff0c;说明锁争用比较严重。 ut_delay这是innodb的一个自旋琐。也就是说&#xff0c;在这里由于锁等待&#xff0c;innodb不停地在…

滑动窗口在重构数据集的作用

step1&#xff1a;使用滑动窗口重构数据集 给定时间序列数据集的数字序列&#xff0c;我们可以将数据重构为看起来像监督学习问题。 我们可以通过使用以前的时间步作为输入变量并使用下一个时间步作为输出变量来做到这一点。 通过观察重构后的数据集与原本的时间序列&…

sliverlight - Unhandled Error in Silverlight Application错误

使用firebug控制台输出错误&#xff1a; Unhandled Error in Silverlight Application 查询“GetFlow_Process”的 Load 操作失败。远程服务器返回了错误: NotFound。 位于 System.ServiceModel.DomainServices.Client.OperationBase.Complete(Exception error) 位于 System.S…

前向验证对于模型的更新作用

首先&#xff0c;让我们看一个小的单变量时间序列数据&#xff0c;我们将用作上下文来理解这三种回测方法&#xff1a;太阳黑子数据集。该数据集描述了刚刚超过 230 年&#xff08;1749-1983 年&#xff09;观察到的太阳黑子数量的每月计数。 数据集显示了季节之间差异很大的…

PHP-面向对象(八)

1、多态的介绍与优势 多态性是继抽象和继承后&#xff0c;面向对象语言的第三个特征。从字面上理解&#xff0c;多态的意思是“多种形态”&#xff0c;简单来说&#xff0c;多态是具有表现多种形态的能力的特征&#xff0c;在OO中是指“语言具有根据对象的类型以不同方式处理。…

双指数平滑中参数对于预测模型的影响

先看看α 在β一致的情况下&#xff0c;α越小&#xff0c;模型越滞后。 再看看β 在α一致的情况下&#xff0c;β越大&#xff0c;模型对于趋势的预测更敏锐。

分页查询

分页查询算是比较常用的一个查询了在DAO层主要是查两个数据第一个总条数第二个要查询起始记录数到查询的条数当第一次点击查询时候(非下一页时Page类里面预设的就是 index就是0 pageSize是预设值当点击下一页的时候 index 和 pageSize带的就是页面上面给的值了分页的页面一般的…

me23n去价格

SELECT knumv kposn AS ebelp kschl kbetr kpein kwert INTO CORRESPONDING FIELDS OF TABLE gt_konv FROM konv FOR ALL ENTRIES IN gt_ekpo WHERE knumv gt_ekpo-knumv AND kinak EQ AND kschl IN (PB00,PBXX,P101).转载于:…

使用Bootstrap-table创建表单,并且与flask后台进行数据交互

文章目录引用css和js使用htmljavascriptflaskmysql参考引用css和js Bootstrap-table为这些文件提供了 CDN 的支持&#xff0c;所以不需要下载.js .css文件就可以直接用了&#xff0c;十分方便 <!-- Latest compiled and minified CSS --> <link rel"stylesheet…

使用Xcode和Instruments调试解决iOS内存泄露

虽然iOS 5.0版本之后加入了ARC机制&#xff0c;但由于相互引用关系比较复杂时&#xff0c;内存泄露还是可能存在。所以了解原理很重要。 这里讲述在没有ARC的情况下&#xff0c;如何使用Instruments来查找程序中的内存泄露&#xff0c;以及NSZombieEnabled设置的使用。 本文假设…

五大主流浏览器 HTML5 和 CSS3 兼容性比较

转眼又已过去了一年&#xff0c;在这一年里&#xff0c;Firefox 和 Chrome 在拼升级&#xff0c;版本号不断飙升&#xff1b;IE10 随着 Windows 8 在去年10月底正式发布&#xff0c;在 JavaScript 性能和对 HTML5 和 CSS3 的支持方面让人眼前一亮。这篇文章给大家带来《五大主流…

精通 VC++ 实效编程280例 - 02 菜单和光标

菜单和关闭时重要的 Windows 资源之一。SDK 中&#xff0c;用 HCURSOR 和 HMENU 分别表示菜单和光标的句柄。MFC 中&#xff0c;CMenu 类封装了菜单的功能。 23 动态添加和删除菜单项 添加菜单项可以调用 CMenu::AppendMenu 或 CMenu::InserMenu 函数&#xff0c;删除菜单项可以…

我的osu游戏程序设计(oo)

osu是一款社区元素为主旨的音乐游戏,由澳大利亚人Dean Herbert (peppy)独立制作并运行. 游戏的方法简单,就是 1. 圈圈(Circle)&#xff1a;圈圈(Circle) 50。没打中显示X,并减少生命值。圈中序号的最后一个的300、100会显示为激300、喝100。2.滑条(Slider) : 在开始端点击按住不…