存储层次


引言

计算机的存储器层级结构是越靠近CPU和CPU关系越密切价格越高容量越小,我们常见的存储器有这几种,速度从快到慢的排序是:寄存器 -> 高速缓存 -> 内存 -> 外部存储器,这一节则针对这几个存储层级进行介绍。

介绍完这几个常见的存储层级组件之后会介绍关于转译后备缓冲区,页面缓存,缓冲区缓存和Linux一些不常见也几乎不使用的调优参数。

存储组件介绍

首先我们来看看不同存储层次的介绍,包括上面提到的寄存器,高速缓存,内存以及他们三者之间的关系。

我们从整体上看一下存储层次结构图:

计组存储层次简析_页面缓存


注意这些参数放到现在都是比较老的了,我们只需要简单理解从左到右速度由最快到快到慢到最慢的递进。


高速缓存:

高速缓存是位于CPU与主内存间的一种容量较小但速度很高的存储器,当内存的数据被读取之后,数据不会直接进入寄存器而是先在高速缓存进行存储,所以读取的大小取决于缓存块的大小,读取速度取决于不同层级高速缓存的容量。

高速缓存的执行步骤如下:

  1. 根据指令把数据读取到寄存器。
  2. 寄存器进行计算操作
  3. 把运算结果传输给内存

在上面的三个步骤中寄存器是基本没有传输消耗的,但是内存的传输就相对于寄存器来说就慢不少了,同时整个运算的瓶颈也是内存的传输速度,所以高速缓存就是用来解决寄存器和内存之间的巨大差异的。

高速缓存分为L1,L2,L3,在讲述理论知识之前,这里先举一个形象一点的例子方便理解:

  • L1 cache:就好像需要工具在我们的腰带上,可以随时取用,所以要获取它的步骤最简单也最快
  • L2 cache:就好像需要的工具放到工具箱里面,我们如果需要获取,要先打开工具箱然后把工具箱的工具挂到腰上才能使用,为什么不能从工具箱取出来再放回去呢?其实思考一下如果你需要频繁使用那得多累呀。另外工具箱虽然比腰上的空间大一点,但是也没有大很多,所以L2 cache 没有比L1 cache大多少。
  • L3 cache:L3相比L1和L2要大非常多,相当于一个仓库,我们获取数据需要自己走到仓库去找工具箱然后放到身边,然后再像是上面那样执行一次,虽然仓库容积很大,但是需要操作的步骤最多,时间开销也最大。

L1 cache下面是L2 cache,L2下面是 L3 cache,可以看到L2和L3都有跟L1 cache一样的问题,要加锁,同步,并且L2比L1慢,L3比L2慢.

我们假设需要读取缓存块是10个字节,高速缓存为50个字节,R0、R1的寄存器总计为20个字节,当R1需要读取某个地址的数据时,在第一次读取数据的时候将10字节先加载到高速缓存,然后再由高速缓存传输到寄存器,此时R0有10字节的数据,如果下次还需要读取10个字节,同样因为高速缓存发现缓存中有相同数据,则直接从高速缓存读取10个字节到R1中。

那么如果此时R0数据被改写会怎么办?首先改写寄存器值之后,会同时改写高速缓存的值,此时如果内存进来缓存块数据,在高速缓存中会先标记这些值,然后高速缓存会在某一个时刻把改写的数据同步到内存中。

如果高速缓存不足的情况下系统会发生什么情况?首先高速缓存会根据一些缓存淘汰机制淘汰末端最少使用的高速缓存,但是如果高速缓存的“变脏”速度很快并且高速缓存的容量总是不足的情况下,会发生内存频繁写入高速缓存并且不断变动高速缓存的情况,此时就会出现可感知的系统抖动。


注意本文讨论的内容全部为回写,改写的方式分为直写回写,回写在高速缓存中存在一定的延迟,利用时间积累的方式定时改写的方式进行内存的同步刷新,而直写的方式则会在高速缓存改变的那一刻立刻改写内存的值。


那么如何衡量访问的局限性呢?

几乎所有的程序都可以分为下面两种情况:

  • 时间局限性:在一定的时间内缓存可能被访问一次,但是可以格一小段时间再一次访问,常见的情况是一个循环中不断取值。
  • 空间局限性:访问一段数据的同时还需要访问它周边的数据情况,有点类似磁盘的预读机制。

如果一个进程可以衡量并且把控好上面两个点,那么基本可以认为是一款优秀的程序,但是现实情况往往不是如此。

寄存器:

寄存器包括指令寄存器(IR)和程序计数器(PC),它属于中央处理器的组成部分,寄存器包含指令寄存器恶化程序计数器,另外在对于加减乘除的操作还包括累加器进行累加操作。 ARM走的是简单指令集不同,而X86走复杂指令集,虽然X86从现在来看是走到了尽头,但是依然发光发热并且占据市场主导地位。 复杂指令集会包含非常多的寄存器完成复杂运算,比如下面一些寄存器:

  • 通用寄存器
  • 标志寄存器
  • 指令寄存器

当然寄存器的部分设计底层的硬件和电路原理才能了解,作者本身水都没有就不来献丑了,如果有感兴趣可以针对寄存器作为深入X86架构的入口。

内存:

内存不仅仅是我们熟知的电脑内存,从广义上来说还包括只读存储,随机存储和高速缓存存储。

这里可能会有疑问,为什么内存使用的最多却不如寄存器和高速缓存呢?那是因为内存不仅仅需要和CPU通信还需要和其他的控制器和硬件打交道,同时如果内存吃紧的时候CPU还需要等待内存传输,当然这也可以反过来解释为什么需要高速缓存和寄存器。

内存除了上面提到的这一点原因还有一个原因是主板的总线带宽是有,并且同样共享给各路使用,比如南桥和其他的一些外接设备等等,同时总线也是需要抢占的,并不是分片使用。

其他补充

和存储层次有关的内容之外在存储层次种还存在一些比较特殊的缓存,比如转译后备缓冲区和页面缓存,

转译后备缓冲区

下面的内容来自百科的解释:

转译后备缓冲器(英语:Translation Lookaside Buffer,​​首字母缩略字​​:TLB),通常也被称为页表缓存转址旁路缓存,为​​CPU​​的一种缓存,由​​内存管理单元​​用于改进​​虚拟地址​​到物理地址的转译速度。目前所有的桌面型及服务器型处理器(如 ​​x86​​)皆使用TLB。TLB具有固定数目的空间槽,用于存放将虚拟地址映射至​​物理地址​​的​​标签页表​​条目。为典型的​​结合存储​​(content-addressable memory,​​首字母缩略字​​:CAM)。其搜索关键字为虚拟内存地址,其搜索结果为物理地址。如果请求的虚拟地址在TLB中存在,CAM 将给出一个非常快速的匹配结果,之后就可以使用得到的物理地址访问存储器。如果请求的虚拟地址不在 TLB 中,就会使用​​标签页表​​进行虚实地址转换,而​​标签页表​​的访问速度比TLB慢很多。有些系统允许​​标签页表​​被交换到次级存储器,那么虚实地址转换可能要花非常长的时间。

进程如果想要访问特殊的数据,可以通过下面提到的方式访问逻辑地址:

  • 对照物理页表通过查表的方式把虚拟地址转物理地址
  • 通过访问对应的物理地址寻找实际的物理地址

如果你是C语言相信应该挺熟悉的,没错这里的操作类似一个二级指针的访问操作,可以看到如果想要高速缓存发挥作用必须是一级指针的查找才有意义。但是二级指针的查找是没有太大意义的。

所以TLB的作用就是这么来的,转译后备缓冲器说白了就是用于加速虚拟地址到物理地址转化的一块特殊空间。目的是为了提高多级嵌套映射查找的速度。

页面缓存

注意上面提到的内容是页表缓存,这里是页面缓存。

页面缓存的作用是什么呢?我们都知道外部的硬件存储速度是最为缓慢的,通常应用程序操作硬盘中的数据都是预先把数据加载到内存再进行操作,然而数据并不是直接从磁盘拷贝到内存的,而是在内存和外部存储设备之间多了一层页面缓存。 页面缓存的读取步骤如下:

  • 进程读取磁盘文本数据,寻找到相关数据之后将内容加载到页面缓存
  • 把页面缓存的内容复制到内存中,此时物理数据和内存以及页面缓存数据一致。
  • 如果需要改写文件文本数据,首先会通知页面缓存标记自己为“脏页”。
  • 如果内存不足则空出空闲的页面缓存给内存使用。
  • 如果页面缓存和内存都不足就需要刷新“脏页”空出空间给内存继续使用。
  • 通常情况下页面缓存会定期刷新缓存回写到磁盘中保持数据同步。

另外需要注意如果页面缓存一直没有进程访问或者使用,页面缓存会一直“膨胀”,另外如果页面缓存和内存一直不够用,就会不断的回写脏页并且产生性能抖动问题。

通过这一点我们也可以知道为什么不建议电脑开很多的应用程序。因为如果如果发生页面缓存回收会产生操作系统用户可以感知的抖动问题。

缓冲区缓存

缓冲区缓存很容易和页面缓存搞混,我们只需要简单理解是对原始磁盘块的临时存储,也就是用来缓存磁盘的数据,通常出现在设备文件直连外部的存储设备,比如我们的U盘读写和外接磁盘的读写等等,这些读写通过缓存区缓存进行管理。

需要注意缓冲区缓存通常不会特别大(20MB 左右),**这样内核就可以把分散的写集中起来,统一优化磁盘的写入,**比如可以把多次小的写合并成单次大的写等等。

Linux中调优参数

了解上面各个组件的内容和细节之后,我们来看几个简单的Linux调优参数。

回写周期: 回写周期可以通过sysctl 的​​vm.dirty_writeback_centisecs​​ 参数调整,但是注意这个值的单位比较特殊,厘秒,这个参数默认设置为500,也就是5秒进行一次回写。


厘秒(英文:centisecond,符号cs),1厘秒 = 100分之1​​秒​​。


当然除非为了实验了解否则不要把这个值设置为0.

除了这个参数之外,还有个百分比的参数,当脏页的数量超过百分比之后就会触发脏页回写的操作防止性能剧烈抖动,下面案例的10代表了10%。

下面是这个参数的内容:

vm.dirty_backgroud_ratio = 10

另外如果想要使用字节的形式控制这个阈值,可以通过参数​​vm.dirty_background_bytes​​制定,如果这个参数为0则代表不开启这个配置。

当然脏页不是允许一直存在的,如果脏页积攒到一定的量的时候,可以通过​​vm.dirty_ratio​​控制到达此百分比只会会阻塞用户进程并且把所有的脏页回写。

同样这个参数也是可以通过字节限制的,参数是​​vm.dirty_bytes​​进行控制。

除了这些不太常用的参数之外,还有一个更为特殊的调优参数: 这个参数的配置用于清空所有的页面缓存,操作方法是向/proc/sys/vm/drop_caches 写入3,为什么是写入3,设计者想这么设计,没有什么理由。

超线程

超线程(HT, Hyper-Threading)是英特尔研发的一种技术,于2002年发布。超线程的技术可以把一个核心伪装成两个核心看待,同时对于单核心的CPU,也可以享受模拟双核心的优惠,当然超线程技术不只是有好处,还有一个明显的缺点是多线程抢占以及线程上下文带来的开销,同时哪怕在最理想的情况下超线程的技术最多也只能提升 20% -30%的,但是这个优化对于当年技术实力有限的情况下的技术优化和性能提升效果是非常显著的。


从此牙膏厂走向了挤牙膏的不归路


总结

本节虽然提到了Linux系统但是实际上更多讲计组的原理为主。