CPU缓存

我们知道CPU的缓存一般是由三级缓存构成,缓存离CPU越近,CPU访问缓存的速度就越快。如下图所示,每个核心都有自己的一、二级缓存,但三级缓存却是一颗 CPU 上所有核心共享的;程序执行时,会先将内存中的数据载入到共享的三级缓存中,再进入每颗核心独有的二级缓存,最后进入最快的一级缓存,之后才会被 CPU 使用。

CPU 访问一次内存通常需要 100 个时钟周期以上,而访问一级缓存只需要 4~5 个时钟周期,二级缓存大约 12 个时钟周期,三级缓存大约 30 个时钟周期。
如果 CPU 所要操作的数据或指令在缓存中,则直接读取,这称为缓存命中。命中缓存会带来很大的性能提升,因此,我们的代码优化目标是提升 CPU 缓存的命中率。虽然在冯诺依曼计算机体系结构中,代码指令与数据是放在一起的,但执行时却是分开进入指令缓存与数据缓存的,因此我们要分开来看二者的缓存命中率。

高效利用数据缓存

我们先来看看如下的代码的输出

uint64_t start = get_time_ms();
static int arrayss[10240][10240];
for (int i = 0; i < 10240; ++i)
{for (int j = 0; j < 10240; ++j){arrayss[i][j] = 0;}
}
SCREEN_ERROR("take time 1: %lu", get_time_ms() - start);
start = get_time_ms();
for (int i = 0; i < 10240; ++i)
{for (int j = 0; j < 10240; ++j){arrayss[j][i] = 0;}
}
SCREEN_ERROR("take time 2: %lu", get_time_ms() - start);
[ERROR] take time 1: 788
[ERROR] take time 2: 4519

可以看到第二段代码的运行效率是第一段的6倍,为什么差距那么大呢?我们知道在C/C++中二维数组在内存中是按行连续存放的。而CPU从内存中加载数据是按CACHE_LINE的大小从内存中加载数据的,典型的CACHE_LINE大小为64字节,可以通过如下的指令查看:

cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
64

那么对于第一段代码,第一次访问arrayss[0][0]时,会一次性把后面的16个元素都加载到缓存中,那么接下来的16次元素访问都不需要从内存中加载,直到访问第17个元素,在一次性加载16个元素。而第二段代码每次访问元素都得从内存中加载16个元素,但只利用了其中的一个。显然第一段代码的缓存利用率提高了16倍。那为什么CPU要一次性加载CACHE_LINE字节的数据到缓存中呢?这主要是因为程序的局部性原理。程序的局部性原理指的是在一段时间内程序的执行会限定在一个局部范围内。这里的“局部性”可以从两个方面来理解,一个是时间局部性,另一个是空间局部性。时间局部性指的是程序中的某条指令一旦被执行,不久之后这条指令很可能再次被执行;如果某条数据被访问,不久之后这条数据很可能再次被访问。而空间局部性是指某块内存一旦被访问,不久之后这块内存附近的内存也很可能被访问。

避免不能高效利用数据缓存

同样先看一段代码

struct FalseSharing
{
volatile int takeIndex;
volatile int putIndex;
}
FalseSharing falseShare = {0, 0};
void falseSharingFunc1()
{
++falseShare.takeIndex;
}
void falseSharingFunc2()
{
++falseShare.putIndex;
}

由于CPU的Cache是按照CACHE_LINE管理的,当 CPU 从内存中加载 takeIndex 的时候,会同时将 putIndex 也加载进 Cache。如下图所示,假设falseSharingFunc1 运行在 CPU1 上,falseSharingFunc2运行在CPU2上面且他们同时在运行,当falseSharingFunc1 执行加1操作时,falseSharingFunc2也执行了加1操作,由于他们各自缓存到了各自的CPU且处于同一个缓存行中,那么当CPU2执行加1时,由于CPU2的缓存行已经失效了,故需要从内存重新加载,从而导致不能高效的利用缓存。这就是伪共享。伪共享指的是由于共享缓存行导致缓存无效的场景。

那么我们怎么可以避免伪共享呢?方案很简单,每个变量独占一个缓存行、不共享缓存行就可以了,具体技术是缓存行填充。我们可以把上面的结构体改成:

struct FalseSharing
{
char padding[60];
volatile int takeIndex;
char padding[60];
volatile int putIndex;
char padding[60];
}

高效利用指令缓存

同样的先看一段代码

#define __SIZE__    (1024000)
static int arrays[__SIZE__];
int num = 0;
for (int i = 0; i < __SIZE__; ++i)
{arrays[i] = get_rand(256);
}
uint64_t start = get_time_ms();
for (int i = 0; i < __SIZE__; ++i)
{if (arrays[i] < 128){++num;}
}
SCREEN_ERROR("take time 1: %lu, num: %d", get_time_ms() - start, num);
for (int i = 0; i < __SIZE__ /2; ++i)
{arrays[i] = get_rand(128);
}
for (int i = __SIZE__ / 2; i < __SIZE__; ++i)
{arrays[i] = get_rand(128) + 128;
}
num = 0;
start = get_time_ms();
for (int i = 0; i < __SIZE__; ++i)
{if (arrays[i] < 128){++num;}
}
SCREEN_ERROR("take time 2: %lu, num: %d", get_time_ms() - start, num);
[ERROR] take time 1: 8, num: 511563
[ERROR] take time 2: 2, num: 512000

可以看出第二段代码的执行效率是第一段的4倍,为什么差距会那么大呢?这是因为循环中有大量的 if 条件分支,而 CPU含有分支预测器。当代码中出现 if、switch 等语句时,意味着此时至少可以选择跳转到两段不同的指令去执行。如果分支预测器可以预测接下来要在哪段代码执行(比如 if 还是 else 中的指令),就可以提前把这些指令放在缓存中,CPU 执行时就会很快。当数组中的元素完全随机时,分支预测器无法有效工作,而当数组前一半的元素都小于128时,分支预测器会动态地根据历史命中数据对未来进行预测,命中率就会非常高。

缓存命中率查看

那么我们怎么查看CPU的缓存命中率呢?我们可以使用linux的perf stat命令查看进程的cpu使用率。
执行 perf stat 可以统计出进程运行时的系统信息。通过 -e 选项指定要统计的事件。

cache-misses 缓存未命中
cache-references 读取缓存次数
L1-dcache-load-misses 一级缓存未命中
L1-dcache-loads 一级缓存读取缓存次数
L1-icache-loads 一级缓存读取缓存次数
L1-icache-load-misses 一级缓存中指令的未命中情况
branch-loads 分支预测的次数
branch-load-misses 分支预测失败的次数

cache-misses 与 cache-references两者相除就是缓存的未命中率,用 1 相减就是命中率。其他也类似。

CPU亲和性

操作系统提供了将进程或者线程绑定到某一颗 CPU 上运行的能力。如 Linux 上提供了 sched_setaffinity 方法实现这一功能

一个CPU的亲合力掩码用一个cpu_set_t结构体来表示一个CPU集合,下面的几个宏分别对这个掩码集进行操作:
CPU_ZERO() 清空一个集合
CPU_SET()与CPU_CLR()分别对将一个给定的CPU号加到一个集合或者从一个集合中去掉.
CPU_ISSET()检查一个CPU号是否在这个集合中
头文件 sched.h
sched_setaffinity(pid_t pid, unsigned int cpusetsize, cpu_set_t *mask)
该函数设置进程为pid的这个进程,让它运行在mask所设定的CPU上。
如果pid的值为0,则表示指定的是当前进程,使当前进程运行在mask所设定的那些CPU上。
sched_getaffinity(pid_t pid, unsigned int cpusetsize, cpu_set_t *mask)
该函数获得pid所指示的进程的CPU位掩码,并将该掩码返回到mask所指向的结构中。如果pid的值为0,表示的是当前进程。

那为什么需要绑定CPU呢?现在操作系统都是按时间片给每个进程分配时间运行,当进程的时间片用完了,就会切换给其他进程使用。若进程 A 在时间片 1 里使用 CPU 核心 1,自然也填满了核心 1 的一、二级缓存,当时间片 1 结束后,操作系统会让进程 A 让出 CPU,基于效率并兼顾公平的策略重新调度 CPU 核心 1,以防止某些进程饿死。如果此时 CPU 核心 1 繁忙,而 CPU 核心 2 空闲,则进程 A 很可能会被调度到 CPU 核心 2 上运行,这样,即使我们对代码优化得再好,也只能在一个时间片内高效地使用 CPU 一、二级缓存了,下一个时间片便面临着缓存效率的问题。
当多线程同时执行密集计算,且 CPU 缓存命中率很高时,如果将每个线程分别绑定在不同的 CPU 核心上,性能便会获得非常可观的提升。perf 工具也提供了 cpu-migrations 事件,它可以显示进程从不同的 CPU 核心上迁移的次数。

如何高效的利用CPU缓存相关推荐

  1. CPU 缓存如何影响你的 Go 程序性能

    小菜刀最近在medium上阅读了一篇高赞文章<Go and CPU Caches>,其地址为https://teivah.medium.com/go-and-cpu-caches-af5d ...

  2. CPU缓存体系对Go程序的影响

    小菜刀最近在medium上阅读了一篇高赞文章<Go and CPU Caches>,其地址为https://teivah.medium.com/go-and-cpu-caches-af5d ...

  3. CPU缓存命中率和缓存行详解

    冯诺依曼计算机 早期的冯诺依曼计算机,大抵功能和工作流程如下: 输入设备接收用户输入的指令信息 数据到达到达运算器,运算器将需要的指令存入存储器中 控制器从存储器中捞数据和指令进行计算再给运算器进行计 ...

  4. 内存对齐与CPU缓存

    公司有小伙伴提出了类似的问题, 根据自己的思路,整理了一下相关的内容,做了一期分享. 目录 一.内存分页/分段管理.内存对齐 1.前置知识点 2.内存分页.分段 4.何为内存对齐 5.为何要有内存对齐 ...

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

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

  6. 简单说一下,你对CPU缓存的了解?

    为什么80%的码农都做不了架构师?>>>    cpu缓存是位于cpu和内存之间的高速缓冲存储器,因为现在cpu的运算速度远远超过了内存的读写速度,因此设置cpu缓存来提高cpu的执 ...

  7. [编程技巧] 巧用CPU缓存优化代码:数组 vs. 链表

    一个常见的编程问题: 遍历同样大小的数组和链表, 哪个比较快? 如果按照大学教科书上的算法分析方法,你会得出结论,这2者一样快, 因为时间复杂度都是 O(n). 但是在实践中, 这2者却有极大的差异. ...

  8. 从Java视角理解CPU缓存(CPU Cache)

    http://coderplay.iteye.com/blog/1485760 众所周知, CPU是计算机的大脑, 它负责执行程序的指令; 内存负责存数据, 包括程序自身数据. 同样大家都知道, 内存 ...

  9. CPU缓存和内存屏障

    CPU性能优化手段-缓存 为了提高程序运行的性能,现代CPU在很多方面对程序进行了优化. 例如:CPU高速缓存.尽可能地避免处理器访问主内存的时间开销,处理器大多会利用缓存(cache)以提高性能. ...

最新文章

  1. Azure手把手系列5:Azure帐户和订阅
  2. 简单建立安装和配置symantec内部LiveUpdate服务器的方法
  3. 使用jsonpath解析json内容
  4. 硬件厂商纷纷“变软”:FPGA行业巨头Xilinx推出Vitis AI平台,并在GitHub上开源
  5. volatile 关键字解析
  6. oracle xml文件是什么文件,介绍关于Oracle下存取XML格式数据的方式
  7. save product in COMMPR01的调试和调用栈
  8. 关于Paxos 幽灵复现问题的看法
  9. 夜雨数竞笔记-极限(4)-Stolz定理
  10. iOS 之电影播放器
  11. 专科生的逆袭之路,比你想象中还要励志
  12. 【目标检测】YOLOv5-PyQT可视化例程开发
  13. java - (二)netty 心跳监测机制
  14. STM32电子钟万年历Proteus仿真_LCD1602显示
  15. ArrList 源码拜读
  16. 技术分享 | 接口自动化测试中如何对xml 格式做断言验证?
  17. 在win10系统中安装一个linux双系统
  18. XSCTF联合招新【Simple-Math】(MSIC+Crypto)
  19. android漏洞检测工具,安卓“超级拒绝服务漏洞”分析及自动检测工具
  20. 怎样修改日立uax规格表_日立UAX-2调试

热门文章

  1. php访问服务器上图片不显示不出来,php显示云服务器上图片不显示图片
  2. access 链接mysql数据库教程_如何在Access中插入超级链接
  3. python项目实战:pyqt5实现登录界面模板
  4. oppor15android版本8.1,OPPO R15体验:基于安卓8.1,ColorOS 5.0更好用
  5. 什么耳机适合华为手机?适合华为耳机的蓝牙耳机推荐
  6. 解决因Docker网桥网段冲突导致访问不到容器问题
  7. Android 1.5到10.0 都有哪些新特性?
  8. js声明数组的几种常见方式
  9. 支付宝支付--alipay.data.dataservice.bill.downloadurl.query(查询对账单下载地址)
  10. 【C语言数组】一、二维数组冒泡排序