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,一经查实,立即删除!

相关文章

textarea 在浏览器中固定大小和禁止拖动

http://blog.sina.com.cn/s/blog_641d569301011naz.html HTML 标签 textarea 在大部分浏览器中只要指定行&#xff08;rows&#xff09;和列&#xff08;cols&#xff09;属性&#xff0c;就可以规定 textarea 的尺寸&#xff0c;大小就不会改变&#xff0c;不过更好的办法是使…

hibernate操作时报错

报错&#xff1a;[ERROR] AbstractBatcher Exception executing batch: org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1原因&#xff1a;视具体情况而定&#xff0c;我这边是代码被修改过…

bugfix:MySQL内存使用率无限增长以及kill手法

问题&#xff1a;昨天mysql 宕机了一次&#xff0c;重启&#xff0c;然后继续运行业务代码的时候发现问题&#xff0c;mysql内存占用率上升较快&#xff0c;于是搜了搜&#xff0c;遇到一个&#xff1a; http://blog.itpub.net/29510932/viewspace-2129312/ 根据思路&#xff0…

软工之初识

我们之前已经在完全不懂软件工程的情况下&#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…

Mysql 遇到的编码问题。

今天帮小朋友做一个项目&#xff0c;碰到一个挺搞的问题。在帮她安装mysql的时候一直是next&#xff0c;没有去注意一些细节&#xff0c;不晓得有没有漏掉设置编码那一部分。。 结果在用sql文件导入数据库MySQL -h localhost -u root -p xxx < e:\xxx.sql 执行的时候错误提…

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

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

py脚本:获取进程信息

这里以mysqld进程为例子 # pip install psutil import psutil import time import re, sys# x:进程name y:非进程name # 由于这里监控的是mysqld&#xff0c;如果不加限制的话会先识别mysqld_safe&#xff0c;所以要加上mysql_safe的判别 def processinfo(x, y):p_list psut…

sysctl -P 报错解决办法

sysctl -P 报错解决办法问题症状修改 linux 内核文件 #vi /etc/sysctl.conf后执行sysctl -P 报错error: "net.bridge.bridge-nf-call-ip6tables" is an unknown keyerror: "net.bridge.bridge-nf-call-iptables" is an unknown keyerror: "net.bridg…

-bash: belts.awk: command not found

执行awk命令时&#xff0c;没有问题。可是执行awk脚本时&#xff0c;出现这个问题&#xff1a;-bash: belts.awk: command not found。 既然之前直接执行awk命令没有问题&#xff0c;说明awk已经装了&#xff0c;本身是没有问题的。那就说明路径不对&#xff0c;执行echo $PATH…

nagios快速安装

1、安装软件包&#xff08;准备软件包&#xff09; yum install httpd gcc glibc glibc-common gd gd-devel 2、建立一个账户 创建一个名为nagios的帐号并给定登录口令 /usr/sbin/useradd nagios passwd nagios 创建一个用户组名为nagcmd用于从Web接口执行外部命令。将nagios用…

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

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

Effective Modern C++翻译(3)-条款2:明白auto类型推导

条款2 明白auto类型推导 如果你已经读完了条款1中有关模板类型推导的内容&#xff0c;那么你几乎已经知道了所有关于auto类型推导的事情&#xff0c;因为除了一个古怪的例外&#xff0c;auto的类型推导规则和模板的类型推导规则是一样的&#xff0c;但是为什么会这样呢&#xf…

信息论与编码复习

若信源有m种消息&#xff0c;且每个消息是以相等可能产生的&#xff0c;则该信源的信息量可表示为Ilogm。 信息率是通过接收到的信息可获得的发送信息的信息量,即互信息。单位:bit/符号。 信息速率是单位时间内传输的信息量。单位:bit/s 码字CodeWord。由若干个码元组成&#x…

拖拽碰撞效果最终版

拖拽碰撞效果最终版&#xff0c;没准还有bug&#xff0c;不过现在在各个浏览器下效果是对的&#xff0c;代码需要精简一些&#xff0c;以后有时间了在弄吧&#xff0c;现在先不理了&#xff0c;感冒了&#xff0c;没有心情整理 <!DOCTYPE HTML> <html lang"en-US…

Python 如何利用函数修改函数外list?

#在函数内修改列表的时候&#xff0c;在列表后面加上[:]&#xff0c;无论几维列表均可。 def foo(listA):listA[:] [1,2,3] def foo2(listB):listB [1,2,3] listA [4,5,6] listB [4,5,6] foo(listA) foo2(listB) print listA #result: [1,2,3] print listB #result: [4,5,6…

图片压缩android bitmap compress(图片压缩)

本文纯属个人见解&#xff0c;是对前面学习的总结&#xff0c;如有描述不正确的地方还请高手指正~ 有些场景中&#xff0c;须要照相并且上传到服务&#xff0c;但是由于图片的巨细太大&#xff0c;那么就 上传就 会很慢(在有些网络情况下)&#xff0c;而且很耗流量&#xff0c;…

linux进程间通信快速入门【一】:管道编程

介绍 管道本质上就是一个文件&#xff0c;前面的进程以写方式打开文件&#xff0c;后面的进程以读方式打开。这样前面写完后面读&#xff0c;于是就实现了通信。虽然实现形态上是文件&#xff0c;但是管道本身并不占用磁盘或者其他外部存储的空间。在Linux的实现上&#xff0c;…

返回长度hdu 1518 square

查了好多资料&#xff0c;发现还是不全&#xff0c;干脆自己整理吧&#xff0c;至少保证在我的做法正确的&#xff0c;以免误导读者&#xff0c;也是给自己做个记载吧&#xff01; 题目的意思是比较明显的&#xff0c;就是当初给你m根木棒&#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 为偶数的时候…