对于存储器,速度越快、能耗会越高、而且材料的成本也是越贵的。
1.存储器分级策略
从需求上讲,我们希望存储器速度快、体积小、空间大、能耗低、散热好、断电数据不丢失。但在现实中,我们往往无法把所有需求都实现。
下面举几个例子,带你深入体会一下,比如:
- 如果一个存储器的体积小,那它存储空间就会受到制约。
- 如果一个存储器电子元件密度很大,那散热就会有问题。因为电子元件都会产生热能,所以电子元件非常集中的 CPU,就需要单独的风扇或者水冷帮助电子元件降温。
- 如果一个存储器离 CPU 较远,那么在传输过程中必然会有延迟,因此传输速度也会下降。
这里你可能会有疑问,因为在大多数人的认知里,光速是很快的,而信号又是以光速传输的。既然光速这么快,那信号的延迟应该很小才对。但事实并不是这样,比如时钟信号是 1GHz 的 CPU,1G 代表 10 个亿,因此时钟信号的一个周期是 1/10 亿秒。而光的速度是 3×10 的 8 次方米每秒,就是 3 亿米每秒。所以在一个周期内,光只能前进 30 厘米。
你看!虽然在宏观世界里光速非常快,但是到计算机世界里,光速并没有像我们认知中的那么快。所以即使元件离 CPU 的距离稍微远了一点,运行速度也会下降得非常明显。
你可能还会问,那干吗不把内存放到 CPU 里?
如果你这么做的话,除了整个电路散热和体积会出现问题,服务器也没有办法做定制内存了。也就是说 CPU 在出厂时就决定了它的内存大小,如果你想换更大的内存,就要换 CPU,而组装定制化是你非常重要的诉求,这肯定是不能接受的。
此外,在相同价格下,一个存储器的速度越快,那么它的能耗通常越高。能耗越高,发热量越大。
既然我们不能用一块存储器来解决所有的需求,那就把需求分级。
Register
最靠近 CPU 的控制单元和逻辑计算单元的存储器,就是寄存器了,它使用的材料速度也是最快的,因此价格也是最贵的,那么数量不能很多。
存储器的数量通常在几十到几百之间,每个寄存器可以用来存储一定的字节(byte)的数据。比如:
-
32 位 CPU 中大多数寄存器可以存储 4 个字节;
-
64 位 CPU 中大多数寄存器可以存储 8 个字节。
寄存器的访问速度非常快,一般要求在半个 CPU 时钟周期内完成读写,CPU 时钟周期跟 CPU 主频息息相关,比如 2 GHz 主频的 CPU,那么它的时钟周期就是 1/2G,也就是 0.5ns(纳秒)。
CPU 处理一条指令的时候,除了读写寄存器,还需要解码指令、控制指令执行和计算。如果寄存器的速度太慢,则会拉长指令的处理周期,从而给用户的感觉,就是电脑「很慢」。
在CPU中,通常有通用寄存器,如指令寄存器IR;特殊功能寄存器,如程序计数器PC、sp等。
Register的作用
CPU上的通用寄存器用于保存和检查CPU的状态。具体来说,是运算中的数据、中断或子例程中程序分支时的返回地址、运算结果为负值、为零时的信息和进位值等。CPU的通用寄存器通过硬件直接连接到CPU,因此它的优点是它比通过内部总线访问数据的RAM更快。
简单的说就是有一定的存储能力,主要的作用还是数据计算
Cache
程序最终会被编译成一组指令,最终由CPU运行。当我们运行程序的时候,这些指令必须放在CPU上执行。为了合理利用CPU的高性能,平衡这内存和CPU间的速度差异。于是就有了在CPU和主内存之间增加缓存的设计,以均衡与内存的速度差异。
CPU的运算速度和内存的访问速度相差比较大。这就导致CPU每次操作内存都要耗费很多等待时间。内存的读写速度成为了计算机运行的瓶颈。
Cache的出现是为了解决CPU直接访问内存效率低下的问题。
- 程序在运行的过程中,CPU接收到指令 后,它会先向CPU中的一级缓存(L1 Cache)去寻找相关的数据,如果命中缓存,CPU进行计算时就可以直接对CPU Cache中的数据进行读取和写入,当运算结束之后,再将CPU Cache中的新数据刷新到主内存当中,CPU通过直接访问Cache的方式替代直接访问主存的方式极大地提高了CPU 的吞吐能 力。
- 但是由于一级缓存(L1 Cache)容量较小,所以不可能每次都命中。这时CPU会继续向下一级的二 级缓存(L2 Cache)寻找,同样的道理,当所需要的数据在二级缓存中也没有的话,会继续转向L3 Cache、内存(主存)和硬盘。
缓存的级别
Level 1 cache
简称 L1 cache
L1 cache 在 CPU 中,相比寄存器,虽然它的位置距离 CPU 核心远,但造价更低。通常 L1 cache 大小在几十 Kb 到几百 Kb 不等,读写速度在 2~4 个 CPU 时钟周期。
L1 cache 通常也分为两种方式,分为指令缓存和数据缓存。
- 指令高速缓存处理有关CPU必须执行的操作的信息,
- 数据高速缓存则保留要在其上执行操作的数据。
L1 高速缓存通常分成指令缓存和数据缓存。
在 Linux 系统,我们可以通过这条命令,查看 CPU 里的 L1 Cache 「数据」缓存的容量大小:
$ cat /sys/devices/system/cpu/cpu0/cache/index0/size
32K
查看 L1 Cache 「指令」缓存的容量大小,则是:
$ cat /sys/devices/system/cpu/cpu0/cache/index1/size
32K
Level 2 cache
简称 L2 cache
L2 cache 也在 CPU 中,位置比 L1-Cache 距离 CPU 核心更远。它的大小比 L1 Cache 更大,具体大小要看 CPU 型号,有 2M 的,也有更小或者更大的,速度在 10~20 个 CPU 周期。
L2 cache一般会保存下一步可能由CPU访问的数据。在大多数现代CPU中,L1和L2高速缓存位于CPU内核本身,每个内核都有自己的高速缓存。
在 Linux 系统,我们可以通过这条命令,查看 CPU 里的 L2 Cache 的容量大小:
$ cat /sys/devices/system/cpu/cpu0/cache/index2/size
256K
Level 3 cache
简称 L3 cache
L3 Cache 同样在 CPU 中,位置比 L2 Cache 距离 CPU 核心更远。大小通常比 L2 Cache 更大,读写速度在 20~60 个 CPU 周期。L3 Cache 大小也是看型号的,比如 i9 CPU 有 512KB L1 Cache;有 2MB L2 Cache; 有16MB L3 Cache。
在 Linux 系统,我们可以通过这条命令,查看 CPU 里的 L3 Cache 的容量大小:
$ cat /sys/devices/system/cpu/cpu0/cache/index3/size
3072K
缓存命中或未命中以及延迟
L1,L2,L3 指的都是CPU的缓存,他们比内存快,但是很昂贵,所以用作缓存,当 CPU 需要内存中某个数据的时候,如果寄存器中有这个数据,我们可以直接使用;如果寄存器中没有这个数据,我们就要先查询 L1 缓存,L1 中没有,再查询 L2 缓存;L2 中没有再查询 L3 缓存;L3 中没有,再去内存中拿。目的就是提高速度。
- 如果在Cache中找到数据,这称为缓存命中。
- 如果在Cache中没有找到数据,它将尝试从主内存访问数据。这称为缓存未命中,也叫穿透(miss),就是一次读取操作没有从缓存中找到对应的数据。
- 从内存访问数据所需的时间称为延迟,L1具有最低的延迟,是最快的,并且最接近核心,而L3具有最高的延迟。缓存未命中时,延迟会增加很多。这是因为CPU必须从主存储器中获取数据。
- 据统计,L1 Cache的命中率在 80% 左右,L1/L2/L3 加起来的命中率在 95% 左右。因此,CPU 缓存的设计还是相当合理的。只有 5% 的内存读取会穿透到内存,95% 都能读取到缓存。
Cache是位于CPU与内存之间的临时存储器,它的容量比内存小但交换速度快。在Cache中的数据是内存中的一小部分,但这一小部分是短时间内CPU即将访问的,当CPU调用大量数据时,就可避开内存,直接从Cache中调用,从而加快读取速度。由此可见,在CPU中加入Cache是一种高效的解决方案,这样整个内存储器(Cache+内存)就变成了既有Cache的高速度,又有内存的大容量的存储系统了。Cache对CPU的性能影响很大,主要是因为CPU的数据交换顺序和CPU与Cache间的带宽引起的。
主流的 CPU 架构
一个 CPU 处理器中一般有多个运行核心,我们把一个运行核心称为一个物理核,每个物理核都可以运行应用程序。
每个物理核都拥有私有的 L1 cache,包括一级指令缓存和一级数据缓存,以及私有的L2 cache。
物理核的私有缓存:它其实是指缓存空间只能被当前的这个物理核使用,其他的物理核无法对这个核的缓存空间进行数据存取。
因为 L1 和 L2 缓存是每个物理核私有的,所以,当数据或指令保存在 L1、L2 缓存时,物理核访问它们的延迟不超过 10 纳秒,速度非常快。
但是,这些 L1 和 L2 缓存的大小受限于处理器的制造技术,一般只有 KB 级别,存不下太多的数据。如果 L1、L2 缓存中没有所需的数据,应用程序就需要访问内存来获取数据。而应用程序的访存延迟一般在百纳秒级别,是访问 L1、L2 缓存的延迟的近 10 倍,不可避免地会对性能造成影响。
所以,不同的物理核还会共享一个共同的三级缓存(Level 3 cache,简称为 L3 cache)。L3 缓存能够使用的存储资源比较多,所以一般比较大,能达到几 MB 到几十 MB,这就能让应用程序缓存更多的数据。当 L1、L2 缓存中没有数据缓存时,可以访问 L3,尽可能避免访问内存。
另外,现在主流的 CPU 处理器中,每个物理核通常都会运行两个超线程,也叫作逻辑核。同一个物理核的逻辑核会共享使用 L1、L2 缓存。
在主流的服务器上,一个 CPU 处理器会有 10 到 20 多个物理核。同时,为了提升服务器的处理能力,服务器上通常还会有多个 CPU 处理器(也称为多 CPU Socket),每个处理器有自己的物理核(包括 L1、L2 缓存),L3 缓存,以及连接的内存,同时,不同处理器间通过总线连接。
下图显示的就是多 CPU Socket 的架构,图中有两个 Socket,每个 Socket 有两个物理核。
在多 CPU 架构上,应用程序可以在不同的处理器上运行。比如上图中,应用程序可以先在 Socket 1 上运行一段时间,然后再被调度到 Socket 2 上运行。
但是,有个地方需要你注意一下:如果应用程序先在一个 Socket 上运行,并且把数据保存到了内存,然后被调度到另一个 Socket 上运行,此时,应用程序再进行内存访问时,就需要访问之前 Socket 上连接的内存,这种访问属于远端内存访问。和访问 Socket 直接连接的内存相比,远端内存访问会增加应用程序的延迟。
在多 CPU 架构下,一个应用程序访问所在 Socket 的本地内存和访问远端内存的延迟并不一致,所以,我们也把这个架构称为非统一内存访问架构(Non-Uniform Memory Access,NUMA 架构)。
到这里,我们就知道了主流的 CPU 多核架构和多 CPU 架构,我们来简单总结下 CPU 架构对应用程序运行的影响。
- L1、L2 缓存中的指令和数据的访问速度很快,所以,充分利用 L1、L2 缓存,可以有效缩短应用程序的执行时间;
- 在 NUMA 架构下,如果应用程序从一个 Socket 上调度到另一个 Socket 上,就可能会出现远端内存访问的情况,这会直接增加应用程序的执行时间。
内存
内存的主要材料是半导体硅,是插在主板上工作的。因为它的位置距离 CPU 有一段距离,所以需要用总线和 CPU 连接。因为内存有了独立的空间,所以体积更大,造价也比上面提到的存储器低得多。现在有的个人电脑上的内存是 16G,但有些服务器的内存可以到几个 T。内存速度大概在 200~300 个 CPU 周期之间。
SSD 和硬盘
SSD 也叫固态硬盘,结构和内存类似,但是它的优点在于断电后数据还在。内存、寄存器、缓存断电后数据就消失了。内存的读写速度比 SSD 大概快 10~1000 倍。以前还有一种物理读写的磁盘,我们也叫作硬盘,它的速度比内存慢 100W 倍左右。因为它的速度太慢,现在已经逐渐被 SSD 替代。
一些优化
缓存行
CPU访问内存时,并不是逐个字节访问,而是以字长为单位访问。比如32位的CPU,字长为4字节,那么CPU访问内存的单位也是4字节。
这么设计的目的,是减少CPU访问内存的次数,加大CPU访问内存的吞吐量。比如同样读取8个字节的数据,一次读取4个字节那么只需要读取2次。若逐个字节访问需要读取8次。
执行程序是靠运行CPU执行主存中代码,但是CPU和主存的速度差异是非常大的,为了降低这种差距,在架构中使用了CPU缓存。数据在CPU缓存(多级cache)中并非是单独存储的,而是按行存储的。其中每一行成为一个缓存行。也就是说缓存里面都是由缓存行组成的,缓存系统中是以缓存行(cache line)为单位存储。
缓存行是2的整数幂个连续字节,每个缓存行的大小一般为2的N次方字节。在32位计算机中为32字节,64位计算机中为64字节。
内存对齐
我们来看看,编写程序时,变量在内存中是否按内存对齐的差异。有2个变量
- word1=a(占用1个字节)
- word2=b(占用4个字节)
我们假设CPU以4字节为单位读取内存。如果变量在内存中的布局按4字节对齐,那么读取a变量只需要读取一次内存,即word1;读取b变量也只需要读取一次内存,即word2。
而如果变量不做内存对齐,那么读取a变量也只需要读取一次内存,即word1;但是读取b变量时,由于b变量跨越了2个word,所以需要读取两次内存,分别读取word1和word2的值,然后将word1偏移取后3个字节,word2偏移取前1个字节,最后将它们做或操作,拼接得到b变量的值。
显然,内存对齐在某些情况下可以减少读取内存的次数以及一些运算,性能更高。另外,由于内存对齐保证了读取b变量是单次操作,在多核环境下,原子性更容易保证。
但是内存对齐提升性能的同时,也需要付出相应的代价。由于变量与变量之间增加了填充,并没有存储真实有效的数据,所以占用的内存会更大。这也是一个典型的空间换时间的应用场景
指令的预读
CPU 顺序执行内存中的指令,CPU 执行指令的速度是非常快的,一般是 26 个 CPU 时钟周期;这节课,我们学习了存储器分级策略,发现内存的读写速度其实是非常慢的,大概有 200300 个时钟周期。
所以CPU增加了指令预读取,CPU 把内存中的指令预读几十条或者上百条到读写速度较快的 L1 Cache中,因为 L1 Cache的读写速度只有 2~4 个时钟周期,是可以跟上 CPU 的执行速度的。
这里又产生了另一个问题:如果数据和指令都存储在L1 Cache中,如果数据缓存覆盖了指令缓存,就会产生非常严重的后果。因此,L1 Cache通常会分成两个区域,一个是指令区,一个是数据区。
与此同时,又出现了一个问题,L1 Cache分成了指令区和数据区,那么 L2/L3 需不需要这样分呢?其实,是不需要的。因为 L2 和 L3,不需要协助处理指令预读的问题。
总结
为了合理利用CPU的高性能,平衡这三者的速度差异,主要体现为:
- CPU增加了缓存,以均衡与内存的速度差异;
- CPU增加了指令预读取,减少与内存的交互次数