冯诺依曼计算机

早期的冯诺依曼计算机,大抵功能和工作流程如下:

  1. 输入设备接收用户输入的指令信息
  2. 数据到达到达运算器运算器将需要的指令存入存储器
  3. 控制器存储器中捞数据和指令进行计算再给运算器进行计算,然后再响应到输出设备
    从这几个步骤中,我们可以感觉到一个很明显的坑,控制流程调度的事情落到了运算器身上,导致了很多没必要的开销。

现代计算机

现在计算机对此进行了改造,可以看出他们将需要处理的数据的运算器和存储器放到两边,然后存储器负责运输数据给这两者,数据经过运算器计算之后再将数据经过存储器再转交给控制器转发到输出设备

了解这样的体系结构后,我们将这些组成部分转换为下图,就得到了现代计算机重要的一个部分-cpu
cpu就包含了运算器和控制器以及主存储器,也就是我们常说的内存。
而辅存就是我们常说的硬盘

了解完现代计算机体系结构后,我们将职责进行划分就构成了下图所示的样子,可以看出由运算器和控制器构成了CPU,主存也就是我们常说的内存,外设部分由一些I/O设备、辅存(就是硬盘)组成。

来聊聊CPU调优

为什么写代码时,要考虑到通过CPU来提升程序性能呢?

如下图所示,可以看出cpu cache的访问速度远远大于内存以及辅存速度。所以我们如果能够尽可能的利用cpu cache来存储我们常访问到的速度。

简述CPU Cache的工作过程

在CPU访问数据的时候,都会优先去缓存中获取数据,如果没数据再去内存取,若内存找不到,则直接取磁盘中找到数据并加载到CPU CACHE中。

CPU CACHE开始加载数据的时候,他也不是一个数组都读起来,而是一小块一小块的进行数据读取。而这一小块数据就是我们日常所说的CPU Line(缓存行)。
我们不妨在Linux中键入如下的命令,我们就能够看到自己CPU L1 Cache对应的CPU Line的大小。以笔者为例,查到的大小为64字节。

cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size # 输出结果64

64字节是什么概念呢?

我们都知道一个整型变量是4个字节。假如我们声明一个数组int data[] = new int[32768];,当CPU cache载入data[0]时,由于cache line的大小为64字节,也就是说还会有一些空闲的空间没用到,根据局部性原理,CPU就会将剩余部分用于存储data[0]附近的数据,也就是说载入data[0]时,cache line顺手将索引1-15的元素也加载到CPU cache line中。

那么问题又来了?当cache中有内存加载的数据时,我们怎么知道这个数据对应的内存那块数据呢?
其实CPU早就考虑到这点了,实现方式也很简单,通过直接映射Cache(Direct Mapped Cache),说白了就是将内存地址CPU cache line做一个映射,我们都知道内存的一块块数据称为内存块,实现地址映射的方式也很简单,就是将内存块地址进行取模运算后再将存到对应的cache line中。
例如我们有8个cache line,32个内存块,当cache需要载入15号块时,cache就会将其存到15%8=7,即7号cache line中。

假如映射地址冲突了怎么办?

这个问题也很简单,增加一个标记就好了,CPU cache给这个标记一个名字Tag
有了tag区分冲突,cpu cache line还需要data存储数据,由于操作系统是多线程运行的,很可能某些数据会在运行期间过期,所以我们也要加一个Valid bit判断这个cache line的数据是否有效,若无效则让CPU别管这个cache line的数据,直接去找内存要数据**(这个工作机制即MESI协议,感兴趣的读者可以自行了解一下)**。

而CPU真正要访问数据的时候,也并不是读取整个cache line的数据,而是读取其中需要的一小部分,这一小部分我们称之为字(Word)。那么我们又该如何找到这个字呢?就是偏移量。
如下图所示,通过取模运算找到对应的cache line,再通过valid bit看看这个cache line是否有效,若有效则继续通过tag找到需要的组,配合偏移量获取的自己所需要的字,CPU就可以开始真正干活了。

(实践)通过CPU的核心原理编写高效的代码

遍历顺序适配CPU缓存加载顺序

先看看下面一段代码,可以看出下面这段代码每一轮都只遍历二维数组的每一行的第一列,在笔者电脑上运行需要5321微秒

public static void main(String[] args) {int[][] arr = new int[10000][10000];long start = System.currentTimeMillis();// 纵向遍历,即外层循环代表二维数组的列,内层循环遍历数组的行,例如下面代码执行到 i=0 j=1,就代表获取以第1行第0个元素for (int i = 0; i < 10000; i++) {for (int j = 0; j < 10000; j++) {arr[j][i] = 0;}}long end = System.currentTimeMillis();System.out.println(end - start);}

再看看这段代码,在笔者电脑上运行只需要119微妙,这是为什么呢?

public static void main(String[] args) {int[][] arr=new int[10000][10000];long start=System.currentTimeMillis();for (int i = 0; i <10000 ; i++) {for (int j = 0; j < 10000; j++) {arr[i][j]=0;}}long end=System.currentTimeMillis();System.out.println(end-start);}

我们不妨想想上文中,cpu cache的原理,由于局部性原理,加载某个数据时会加载其附近的数据,第一段代码遍历数据是纵向遍历的。这就意味着CPU CACHE中的数据不一定有我们for循环所需要的数据。
。就像下图一样,cpu cache只会顺序加载其附近的数据,假如我们取得arr[0] [0],那么他就会顺序加载arr[0] [1]、arr[0] [2]、arr[0] [3]。这就导致遍历过程中只有第一次遍历的列在缓存中,其他列的数据都要去内存中取。

而第二段代码遍历顺序和局部性原理加载顺序是一致的,所需效率自然高了。

提升指令缓存命中率提高效率

我们需要编写这样一段代码,这段代码实现的事情很简单。随机生成一个一维数组,然后遍历判断其ASCII码值是否大于128,若大于128则进行相加。然后排序输出。
先看看第一段代码,实现过程时,先计算,在排序。这段代码在笔者电脑执行时间为10.4748198s

 public static void main(String[] args) {// Generate dataint arraySize = 32768;int data[] = new int[arraySize];Random rnd = new Random(0);for (int c = 0; c < arraySize; ++c)data[c] = rnd.nextInt() % 256;// Testlong start = System.nanoTime();long sum = 0;for (int i = 0; i < 100000; ++i) {// Primary loopfor (int c = 0; c < arraySize; ++c) {if (data[c] >= 128)sum += data[c];}}// !!! With this, the next loop runs fasterArrays.sort(data);System.out.println((System.nanoTime() - start) / 1000000000.0);System.out.println("sum = " + sum);}

再看看第二段代码,先排序再计算。神奇的是这段代码只需要4.5811642s

 public static void main(String[] args) {// Generate dataint arraySize = 32768;int data[] = new int[arraySize];Random rnd = new Random(0);for (int c = 0; c < arraySize; ++c)data[c] = rnd.nextInt() % 256;// Testlong start = System.nanoTime();long sum = 0;// !!! With this, the next loop runs fasterArrays.sort(data);for (int i = 0; i < 100000; ++i) {// Primary loopfor (int c = 0; c < arraySize; ++c) {if (data[c] >= 128)sum += data[c];}}System.out.println((System.nanoTime() - start) / 1000000000.0);System.out.println("sum = " + sum);}

这是为什么呢?
原因其实也很简单,CPU中有一个分支预测器,他会动态根据代码执行if else逻辑的命中数,决定是否将某个分支代码加载到cache中。就以上面两段代码为例,第一段代码数组毫无规律,导致分支预测器无法准确预测,所以cache中的指令不一定是将要执行的分支代码,代码段二反之。

基于现场绑定CPU实现多核 CPU 的缓存命中率

在单核 CPU,虽然只能执行一个进程,但是操作系统给每个进程分配了一个时间片,时间片用完了,就调度下一个进程,于是各个进程就按时间片交替地占用 CPU,从宏观上看起来各个进程同时在执行。

而现代 CPU 都是多核心的,进程可能在不同 CPU 核心来回切换执行,这对 CPU Cache 不是有利的,虽然 L3 Cache 是多核心之间共享的,但是 L1 和 L2 Cache 都是每个核心独有的,如果一个进程在不同核心来回切换,各个核心的缓存命中率就会受到影响,相反如果进程都在同一个核心上执行,那么其数据的 L1 和 L2 Cache 的缓存命中率可以得到有效提高,缓存命中率高就意味着 CPU 可以减少访问 内存的频率。

当有多个同时执行「计算密集型」的线程,为了防止因为切换到不同的核心,而导致缓存命中率下降的问题,我们可以把线程绑定在某一个 CPU 核心上,这样性能可以得到非常可观的提升。

在 Linux 上提供了 sched_setaffinity 方法,来实现将线程绑定到某个 CPU 核心这一功能

参考文献

2.3 如何写出让 CPU 跑得更快的代码?

深入理解CPU的分支预测(Branch Prediction)模型

CPU缓存命中率和缓存行详解相关推荐

  1. 计算机缓存Cache以及Cache Line详解

    转载: 计算机缓存Cache以及Cache Line详解 - 围城的文章 - 知乎 https://zhuanlan.zhihu.com/p/37749443 L1,L2,L3 Cache究竟在哪里? ...

  2. 高并发架构系列:Redis缓存和MySQL数据一致性方案详解

    需求起因 在高并发的业务场景下,数据库大多数情况都是用户并发访问最薄弱的环节.所以,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问MySQL等数据库. 这个业务场景,主要 ...

  3. Spring三级缓存解决循环依赖问题详解

    spring三级缓存解决循环依赖问题详解 前言 这段时间阅读了spring IOC部分的源码.在学习过程中,自己有遇到过很多很问题,在上网查阅资料的时候,发现很难找到一份比较全面的解答.现在自己刚学习 ...

  4. python的pytest模块:pytest命令行详解

    一.官方文档 How to invoke pytest - pytest documentationhttps://docs.pytest.org/en/latest/how-to/usage.htm ...

  5. pandas 如何删掉第一行_pandas删除指定行详解

    pandas删除指定行详解 在处理pandas的DataFrame中,如果想像excel那样筛选,只要其中的某一行或者几行,可以使用isin()方法来实现,只需要将需要的行值以列表方式传入即可,还可传 ...

  6. GCC 命令行详解 -L -l

    我们用gcc编译程序时,常常会用到"-I"(大写i),"-L"(大写l),"-l"(小写l)等参数,下面做个记录: GCC 命令行详解 -L ...

  7. Redis系列教程(六):Redis缓存和MySQL数据一致性方案详解

    需求起因 在高并发的业务场景下,数据库大多数情况都是用户并发访问最薄弱的环节.所以,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问MySQL等数据库. 这个业务场景,主要 ...

  8. CUP 三级缓存L1 L2 L3 cahe详解

    一   三级缓存(L1.L2.L3)是什么? 以近代CPU的视角来说,三级缓存(包括L1一级缓存.L2二级缓存.L3三级缓存)都是集成在CPU内的缓存,它们的作用都是作为CPU与主内存之间的高速数据缓 ...

  9. 分布式缓存redis 方案_Redis缓存和MySQL数据一致性方案详解

    在高并发的业务场景下,数据库大多数情况都是用户并发访问最薄弱的环节.所以,就需要使用redis做一个缓冲操作,让请求先访问到Redis,而不是直接访问MySQL等数据库. 这个业务场景,主要是解决读数 ...

最新文章

  1. cisco 恢复出厂设置
  2. 如何运用python爬游戏皮肤_Python爬虫实战之 爬取王者荣耀皮肤
  3. 从Ubuntu命令行按进程名称杀死进程
  4. 江西理工大学 微型计算机原理,江西理工大学-微机原理考试(wenwei)作业.docx
  5. 第四章 第四节 per_cpu
  6. 创建组件“ovalshape”失败_Django的forms组件检验字段\渲染模板
  7. 计算机辅助英语教学 研究背景,计算机辅助外语教学中的教师角色研究
  8. android 9.0user版本如何开启root,打开su
  9. MyBatis动态SQL
  10. 用python代码辅助自己背诵英语四级单词
  11. Hexo 好看且实用的主题推荐
  12. ubuntu16.04掉显卡驱动解决方法
  13. Arduino基础入门二之呼吸灯
  14. 凛冬至,这一杯互联网咖啡能热多久?
  15. MD5加密是什么?为什么不可解密?
  16. VisualVM 启动报错Error Starting VisualVM:You are running VisualVM using Java Runtime Environment(JRE)
  17. pandas 基础操作
  18. 招聘java是什么意思_企业招聘Java程序员的标准到底是什么?
  19. No adapter attached; skipping layout
  20. [转载]我们为什么不能虐待动物

热门文章

  1. 英语语法01——基础
  2. Jquery之ShowLoading遮罩组件
  3. shell 一行写一个for循环
  4. 短视频实战全攻略:从0开始打造爆款抖音号
  5. 信息学奥赛一本通:2069:【例2.12 】糖果游戏
  6. 华为首席开源联络官执笔,带你了解5G时代的边缘计算
  7. 【CSS学习总结】边框阴影:box-shadow
  8. Stave插件,让Fiddler能将URL映射到本地目录,实现批量文件自动响应
  9. RIME中州韵输入法引擎学习
  10. Python基础——第二章:Python基础语法