【操作系统入门到成神系列 三】如何写出让CPU跑出更快的代码?

简介: 【操作系统入门到成神系列 三】如何写出让CPU跑出更快的代码?

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

代码都是由 CPU 跑起来的,我们代码写的好与坏就决定了 CPU 的执行效率,特别是在编写计算密集型的程序,更要注重 CPU 的执行效率,否则将会大大影响系统性能。

CPU 内部嵌入了 CPU Cache(高速缓存),它的存储容量很小,但是离 CPU 核心很近。

所以缓存的读写速度是极快的,那么如果 CPU 运算时,直接从 CPU Cache 读取数据,而不是从内存的话,运算速度就会很快。


6316dca6a3cd48cdb28e40a2378c1e3c.png

一、引言

本文参考 小林coding 的《图解操作系统》,也是我十分喜欢的一个公众号博主,为他打 call

老读者知道我之前再写 Kafka 的博文,为什么突然开始写操作系统的呢?

原因在于:当我看到 Kafka 服务端的一些 IO 操作时,我发现我看不懂了,了解之后发现这里 Netty 的概念。

当我尝试了解 IO 时,我发现一些内存、磁盘的交换,搞的我焦头烂额,于是,想静下心来从头开始。

当我把 小林coding 的 《图解操作系统》看完之后,我发现对操作系统的理解更上一层楼。

用一段话,作为今天的开场白:

读书的根本目的,未必是解决现实问题,它更像一场心灵的抚慰。

一个喜欢读书的人,可能不会记得自己读过哪些书。

但是那些看过的故事、收获的感悟、浸染过的气质,就像一颗种子,会在你的身体里慢慢发芽长大,不断提升你的认知,打开你的视野。

二、CPU Cache 有多快

为什么有内存还需要 CPU Cache?

  • 根据摩尔定律,CPU的访问速度每 18 个月就会翻倍,内存的速度也会不断增长,但是增长的速度远远小于CPU,于是CPU和内存的访问性能被不断的拉大
  • 了弥补 CPU 与内存两者的差距,在 CPU 内部引进了 CPU Cache,也被成为高速缓存

CPU Cache 通常分为大小不等的三级缓存,分别是 L1 Cache、L2 Cache 和 L3 Cache


150fa671202682b13d7fe94b0c193df4.png

CPU 访问高速缓存的时间:

  • L1 Cache:需要 2~4 个时钟周期
  • L2 Cache:需要 10~20 个时钟周期
  • L3 Cache:需要 20~60 个时钟周期
  • 内存:需要 200~300 个时钟周期

ad4e9f445fd28ab97c48c96d5252dfa1.png

三、CPU Cache 的数据结构和读取过程是什么样的

CPU Cache 的数据是从内存中读取过来的,以一小块一小块的读取数据,在CPU Cache 中,这样的一小块数据被称为:Cache Line(缓存块)

一般来说,缓存块的大小为:64字节。也就意味着 L1 Cache 一次载入数据的大小是 64 字节

L1 Cache 一次性载入数据大小为 64 字节有什么作用?

在 Java 中,如果我们开辟一个 int[100] 的数组,每一个数组元素的大小为 4 字节。

相当于我们访问 int[0] 的数据,实际上 int[1]~int[15] 的数据都会被加载到我们的 CPU Cache 中。

因此,当我们下次访问这些数据时,直接从 CPU Cache 中查询,大大的提升了 CPU 读取数据的性能

我们的 CPU 是怎么知道内存的数据在不在 CPU Cache 中呢?

1. 直接映射 Cache

前面我们提到过,CPU 访问内存数据时,是一小块一小块的读取的,这一小块的大小取决于:coherency_line_size

这里需要记住:

  • 在内存中,这一小块被称为:内存块
  • 在 CPU Cache 中,这一小块被称为:缓存块(Cache Line)
  • 直接映射的策略:将我们内存块的地址映射到一个缓存块中的地址,映射实现的方式为【取模运算】,取模运算的结果就是内存块地址所对应的缓存块的地址。

比如:内存被划分 32 个内存块,CPU Cache 共有 8 个缓存块。

假如我们的CPU想要访问第 15 号内存块,如果我们内存的数据已经被缓存到 CPU Cache 中,那么必定在第 7 号缓存块。因为 15 % 8 = 7

为了区分不同的内存块,在对应的缓存块中还会存储一个 组标记(Tag)。根据这个组标记来区分当前缓存块中的内存块的地址。

当然,除了组标记外,缓存块还有两个信息:

  • 从内存加载过来实际存放的数据(data)
  • 有效位(Valid bit):用来标记当前的缓存块的数据是否有效。如果为 0,则缓存块无效,会读取内存。

当然,我们的 CPU 从缓存块中读取数据时,并不是读取整个缓存块,而是读取 CPU 所需要的一个数据片段,这样的数据被称为一个 字(Word)

问题来了,我们怎么能找到这个 字 呢?答案是:需要一个偏移量(offset)

因此,一个内存的访问地址,包括 组标记(Tag)、缓存块索引(Index)、偏移量 这三种信息,于是 CPU 能够通过这些信息,在缓存块中找到缓存的数据。

而对于 CPU Cache 的数据结构,则是由 索引 + 有效位 + 组标记 + 数据块

024a753ec4009ac8812abc81ae1f244c.png


如果内存的数据已经在 CPU Cache 中了,我们的CPU会得到内存地址的信息(组标记、索引、偏移量)。

那 CPU 访问一个内存地址的时候,会经历以下 4 个步骤:

  • 根据我们内存地址中的【索引】进行取模运算,计算出缓冲块中的索引,也就是缓冲块的地址。
  • 找到对应的缓存块后,需要判断当前缓存块的有效位
  • 如果是无效的,则直接访问内存,刷新数据
  • 如果是有效的,则继续往下执行
  • 根据我们内存地址的组标记找到缓存块中的组标记
  • 如果不相等,则直接访问内存,刷新数据
  • 如果相等,则继续往下执行
  • 根据内存地址的偏移量,从我们的缓存块中读取对应的 字(word)

四、如何写出让CPU跑的更快的代码

我们想一下,怎么才能写出让 CPU 跑的更快的代码?如果我们想要访问的数据在 CPU Cache 中,那么我们 CPU 的速度也会跑的越快。

L1 Cache 通常分为数据缓存和指令缓存,这是因为 CPU 会分别处理数据和指令,比如 1 + 1 = 2 这个运算,

+ 是指令,会被放在 指令缓存 中,而输入的数字 1 会被放在数据缓存中。

1. 如何提升数据缓存的命中率

// 二维数组
int N = 100;
array[N][N] = 0;
// 样例一:
for(int i = 0; i < N; i++){
    for(int j = 0; j < N; j++){
        array[i][j] = 0;
    }
}
// 样例二:
for(int i = 0; i < N; i++){
    for(int j = 0; j < N; j++){
        array[j][i] = 0;
    }
}

我们猜测一下上述样例一和样例二当中,哪一个样例的速度比较快?

经过测试,样例一 array[i][j] 执行时间比样例二 array[j][i] 快好几倍。

因为我们上面讲过,CPU Cache 会一次性加载 64KB 的顺序数据到缓存中, 我们样例一的数据为顺序数组数据,样例二为跳跃数组数据。

因此,遇到这种遍历数组的情况时,按照内存布局顺序访问,将可以有效的利用 CPU Cache 带来的好处,这样我们代码的性能就会得到很大的提升,

2. 如何提升指令缓存的命中率

有如下数组:

// 当前数组的值在 0~99 进行随机
int array[N];
for(int i = 0; i < N; i++){
    array[i] = random(100);
}

接下来对这个数组做两个操作:

// 操作一:数组遍历
for(int i = 0; i < N; i++){
    if(array[i] < 50){
        array[i] = 0;
    }
}
// 操作二:排序
sort(array, array + N);

有一个问题:你觉得是先遍历再排序速度快,还是先排序再遍历速度快呢?

CPU的分支预测器:如果分支预测可以预测到接下来要执行 if 里的指令,还是 else 指令的话,就可以「提前」把这些指令放在指令缓存中,这样 CPU 可以直接从 Cache 读取到指令,于是执行速度就会很快

通过上面的描述,我们如果元素是随机的,我们的分支预测器无法有效的工作,而当数组元素是顺序的,分支预测器会动态的根据历史数据对未来进行预测,这样命中率就会很高。

所以,答案是:先排序再遍历速度快

我们排序之后,刚开始数组的数据都小于 50,会疯狂的命中我们 if 指令,于是分支预测器将 if 指令放入指令缓存中,大大的提高运算的速度。

3. 如何提升多核CPU的缓存命中率

现代 CPU 都是多核心的,线程可能在不同 CPU 核心来回切换执行,这对 CPU Cache 不是有利的,虽然 L3 Cache 是多核心之间共享的,但是 L1 和 L2 Cache 都是每个核心独有的,如果一个线程在不同核心来回切换,各个核心的缓存命中率就会受到影响

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






相关文章
|
4天前
|
存储 算法 安全
深入理解操作系统:从基础概念到代码实践
【9月更文挑战第23天】本文将带领读者深入探索操作系统的奥秘,从基础概念出发,逐步揭示操作系统的工作原理和设计哲学。我们将通过实际代码示例,展示操作系统如何与硬件交互、管理资源以及提供用户界面。无论你是计算机专业的学生还是对操作系统感兴趣的开发者,这篇文章都将为你打开一扇通往操作系统世界的大门。
33 16
|
4月前
|
算法 搜索推荐 开发工具
探索代码的奥秘:技术感悟与实践探索操作系统的心脏:内核
【5月更文挑战第31天】在数字世界的编织中,每一行代码都承载着创造者的智慧和汗水。本文将带你深入编程的核心,揭示那些隐藏在日常开发实践中的技术真谛。从算法的精妙到系统的架构,我们将一同探讨如何通过技术提升效率,解决问题,并在这个过程中获得个人成长。 【5月更文挑战第31天】本文深入剖析了操作系统的核心组件——内核,探讨了其设计哲学、功能职责以及在现代计算环境中的重要性。通过分析内核的工作原理和它如何与硬件、软件交互,我们将揭示这个隐藏在用户界面背后的力量之源。
|
24天前
|
存储 算法 Unix
探索操作系统:从理论到代码
【9月更文挑战第3天】操作系统是计算机的核心,它管理着硬件和软件之间的交互。本文将从理论出发,深入探讨操作系统的基本原理和功能,然后通过代码示例,展示操作系统是如何在实际中运作的。无论你是初学者还是有经验的开发者,这篇文章都将为你提供新的视角和深度理解。
|
25天前
|
调度
CPU调度器实现提示:针对特定体系结构代码【ChatGPT】
CPU调度器实现提示:针对特定体系结构代码【ChatGPT】
|
29天前
|
数据采集 数据可视化 数据挖掘
使用Python进行数据分析的新手指南深入浅出操作系统:从理论到代码实践
【8月更文挑战第30天】在数据驱动的世界中,掌握数据分析技能变得越来越重要。本文将引导你通过Python这门强大的编程语言来探索数据分析的世界。我们将从安装必要的软件包开始,逐步学习如何导入和清洗数据,以及如何使用Pandas库进行数据操作。文章最后会介绍如何使用Matplotlib和Seaborn库来绘制数据图表,帮助你以视觉方式理解数据。无论你是编程新手还是有经验的开发者,这篇文章都将为你打开数据分析的大门。
|
30天前
|
测试技术 数据安全/隐私保护 Python
探索Python中的装饰器:简化代码,增强功能深入理解操作系统:从用户空间到内核空间的旅程
【8月更文挑战第29天】本文将引导你深入理解Python装饰器的核心概念、应用场景及其对代码的优化作用。我们将从基础使用到高级应用逐步展开,通过实例展示如何利用装饰器提升代码的可读性和复用性,同时避免常见的陷阱。
|
2月前
|
机器学习/深度学习 TensorFlow API
Keras是一个高层神经网络API,由Python编写,并能够在TensorFlow、Theano或CNTK之上运行。Keras的设计初衷是支持快速实验,能够用最少的代码实现想法,并且能够方便地在CPU和GPU上运行。
Keras是一个高层神经网络API,由Python编写,并能够在TensorFlow、Theano或CNTK之上运行。Keras的设计初衷是支持快速实验,能够用最少的代码实现想法,并且能够方便地在CPU和GPU上运行。
|
2月前
|
Linux
Linux02---命令基础 Linux命令基础, ls命令入门,ls命令参数和选项,命令行是一种以纯字符操作系统的方式,command命令本身,options命令的细节行为,parameter命令的
Linux02---命令基础 Linux命令基础, ls命令入门,ls命令参数和选项,命令行是一种以纯字符操作系统的方式,command命令本身,options命令的细节行为,parameter命令的
|
2月前
|
Linux 调度
部署02-我们一般接触的是Mos和Wimdows这两款操作系统,很少接触到Linux,操作系统的概述,硬件是由计算机系统中由电子和机械,光电元件所组成的,CPU,内存,硬盘,软件是用户与计算机接口之间
部署02-我们一般接触的是Mos和Wimdows这两款操作系统,很少接触到Linux,操作系统的概述,硬件是由计算机系统中由电子和机械,光电元件所组成的,CPU,内存,硬盘,软件是用户与计算机接口之间
|
3月前
|
并行计算 异构计算 Python
python代码torch.device("cuda:0" if torch.cuda.is_available() else "cpu")是什么意思?
【6月更文挑战第3天】python代码torch.device("cuda:0" if torch.cuda.is_available() else "cpu")是什么意思?
274 4