文章目录

  • 引子
  • 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.8s 7.7s 1.01:1
随机访问 90s 80s 1.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、变量保持地址对齐

CPU Cache对于并发编程的影响相关推荐

  1. Java专家系列:CPU Cache与高性能编程

    认识CPU Cache CPU Cache概述 随着CPU的频率不断提升,而内存的访问速度却没有质的突破,为了弥补访问内存的速度慢,充分发挥CPU的计算资源,提高CPU整体吞吐量,在CPU与内存之间引 ...

  2. CPU Cache与高性能编程

    目录 认识CPU Cache CPU Cache概述 Cache Line伪共享及解决方案 Cache Line伪共享分析 Cache Line伪共享处理方案 Padding 方式 Contended ...

  3. 聊聊高并发(五)理解缓存一致性协议以及对并发编程的影响

    Java作为一个跨平台的语言,它的实现要面对不同的底层硬件系统,设计一个中间层模型来屏蔽底层的硬件差异,给上层的开发者一个一致的使用接口.Java内存模型就是这样一个中间层的模型,它为程序员屏蔽了底层 ...

  4. 聊聊并发编程的10个坑

    前言 对于从事后端开发的同学来说,并发编程肯定再熟悉不过了. 说实话,在java中并发编程是一大难点,至少我是这么认为的.不光理解起来比较费劲,使用起来更容易踩坑. 不信,让继续往下面看. 今天重点跟 ...

  5. Java并发编程简介

    并发编程简介 1. 什么是并发编程 所谓并发编程是指在一台处理器上"同时"处理多个任务.并发是在在同一实体上的多个事件.多个事件在同一时间间隔发生. 并发编程 ①从程序设计的角度来 ...

  6. 基于JVM原理、JMM模型和CPU缓存模型深入理解Java并发编程

    许多以Java多线程开发为主题的技术书籍,都会把对Java虚拟机和Java内存模型的讲解,作为讲授Java并发编程开发的主要内容,有的还深入到计算机系统的内存.CPU.缓存等予以说明.实际上,在实际的 ...

  7. 高并发编程-通过volatile重新认识CPU缓存 和 Java内存模型(JMM)

    文章目录 概述 volatile定义 CPU缓存 相关CPU术语 CPU缓存一致性协议MESI 带有高速缓存的CPU执行计算的流程 CPU 多级的缓存结构 Java 内存模型 (JMM) 线程通信的两 ...

  8. 并发编程-02并发基础CPU多级缓存和Java内存模型JMM

    文章目录 CPU多级缓存 CPU多级缓存概述 CPU 多级缓存-缓存一致性协议MESI CPU 多级缓存-乱序执行优化-重排序 JAVA内存模型 (JMM) 计算机硬件架构简易图示 JAVA内存模型与 ...

  9. 深入理解CPU cache:组织、一致性(同步)、编程

    <CPU Cache Line:CPU缓存行/缓存块> <CPU Cache Line伪共享问题的总结和分析> <内存管理:Linux Memory Management ...

最新文章

  1. 利用堆排序查找数组中第K小的元素方法
  2. Codeforces Round #632 (Div. 2) F. Kate and imperfection 数论 + 贪心
  3. gvim常用的配置及插件 -windows
  4. 【机器学习】机器学习从零到掌握之六 -- 教你使用验证分类器测试算法
  5. Atitit.每周计划日程表 流程表 v9 r829.docx
  6. Julia: using Gadfly using Cairo的一个郁闷的问题!
  7. iOS 13问题记录
  8. java生成密码字典
  9. 项目开发文档编写规范【附文档模板】
  10. windows下的Zcash钱包(ZEC钱包)-zcash4win 1.0.11
  11. FX系列PLC编程手册
  12. 《浪潮之巅》11~14章
  13. R:应用时间序列分析--基于R(2)第二章 时间序列的预处理
  14. rabbitmq:publisher confirms
  15. 可解释深度学习:从感受野到深度学习的三大基本任务:图像分类,语义分割,目标检测,让你真正理解深度学习
  16. Java中的字节是什么意思?
  17. 上海税前12000税后多少_税前12000元月工资,税后能拿多少
  18. 七月上(歌词背后的故事)
  19. surface pro 7 使用type c耳机问题
  20. ubuntu安装pandas

热门文章

  1. 计算机主机风扇安装方法,电脑机箱怎么安装风扇减震胶钉保护主板cup?
  2. 正则表达式贪婪与非贪婪模式
  3. 面试整理(1):原生ajax
  4. 解决微信小程序的wx-charts插件tab切换时的显示会出现位置移动问题-tab切换时,图表显示错乱-实现滑动tab
  5. 移动spa商城优化记(一)---首屏优化篇
  6. CSS3中的圆角边框属性详解(border-radius属性)
  7. 前端学习笔记--HTTP缓存
  8. ArcGIS中的WKID(转)
  9. 利用爬虫模拟网页微信wechat
  10. js unix时间戳转换