CSAPP-存储器的层次结构

到目前为止,在我们对系统的研究中,我们都是依赖于一个简单的计算机系统模型,CPU执行指令,而存储系统为CPU存放指令和数据。。在简单模型中,存储器系统是一个线性的字节数组,而CPU能够在一个常数时间内访问每个存储器的位置。这并没有正确反应现代系统实际的工作方式。

在现代的系统结构中,*存储器系统(memory system)是一个具有不同容量、成本和访问时间的存储设备的层次结构。这里CPU寄存器中保存着最常用的数据。最靠近CPU的小的、快速的高速缓存存储器(cache memory)*最为一部分存储在相对较慢的主存储器(main memory)中的数据和指令的缓冲区域。主存缓存存储在容量较大的、慢速磁盘上的数据,而这些磁盘又常常作为存储在通过网络连接的其他机器上的磁盘或者磁带上的数据缓冲区域。

作为程序员,需要理解存储器的层次结构,因为它对应用程序的性能有着巨大的影响。如果你的数据存储在CPU的寄存器中,那么在指令执行的时候,0个周期内就可以访问到它们。如果存储在高速缓存中,需要4~75个周期。如果存储在主存储器中,需要上百个周期。而如果存储在磁盘上,需要大约几千万个周期。

这里同时还反应了计算机设计的一个基本思想:如果你理解系统是如何将数据在存储器的层次结构中上下移动的,这样对于自己编写的程序,就可以尽量让它的数据存放在存储层次结构中比较高的位置,使得CPU可以更加容易的访问到这些数据。

这里我们将首先了解一下基本的存储技术:SRAM,DRAM,ROM以及机械硬盘和固态硬盘,并描述它们是如何被组织的。

1. 存储技术

1.1 随机访问存储器

随机访问存储器可以被分成两类:SRAM和DRAM,其中SRAM的价格要高于DRAM,SRAM主要被用来作为高速缓存存储器。DRAM用来作为主存。

1.1.1 静态RAM

对于SRAM只要有电,它就会永远的保持它的值。只有两个稳定的配置或者状态。

1.1.2 动态RAM

对于DRAM每一个存储位是一个微小的电容器,所以DRAM存储器对干扰非常敏感。当电容器的电压被扰乱后,他就永远不会恢复。同时还会有许多种原因导致漏电,内存系统会周期性质的通过读出,然后重新来刷新内存中的每一位,来纠错一个字中任何个单位的错误位。

1.1.3 传统DRAM

传统的DRAM芯片中的单元(位) 被分成d个*超单元(supercell),每个超单元都由w个DRAM单元组成。一个dw的DRAM共存储了dw位信息。

DRAM芯片会被连接到CPU中一个被称为*内存控制器(memory controller)*的电路,这个电路可以一次性传送w位到每个DRAM芯片或者一次从每个DRAM芯片传出w位。

因为DRAM是通过二维矩阵的方式排列超单元的。对于内存控制器来说为了读取超单元(i,j)中的内容,内存控制器将行地址i和列地址j发送给DRAM。DRAM将超单元(i,j)的内容发送回控制器作为响应。这里行地址i称为RAS(Row Access Strobe,行访问选通脉冲)请求;列地址j称为CAS(Column Access Strobe,列访问选通脉冲)请求。这里一般来说RAS和CAS请求共享DRAM地址引脚。

例如对于内存控制器的请求读出DRAM中的超单元(2,1),内存控制器会发送地址2。而此时DRAM的响应是将行2的整个内容都复制到一个内部行缓冲区中。接下来内存控制器发送列地址1,DRAM的响应是从缓冲区中复制超单元,并将它们发送到内存控制器。

1.1.4 内存模块

DRAM芯片封装在内存模块(memory module)中,同时被插到主板扩展槽上。

1.1.5 增强的 DRAM

RAM的生产厂商为了试图跟上迅速增长的处理器速度,出现了下面几种类型的增强型的DRAM:

  • *快页模式DRAM(Fast Page Mode DRAM,FPM DRAM)。FPM DRAM允许对同一行连续的访问可以直接从行缓冲区中的到服务。
  • *扩展数据输出DRAM(Extended Data Out DRAM, EDO DRAM)。FPDM DRAM的一个增强形式,允许各个CAS信号在时间上靠的更紧密一点。
  • *同步DRAM(Synchronous DRAM, SDRAM)。这种方式与内存控制器通信使用一组显式的控制信号,常规来说、FPM和EDO DRAM都是异步信号。同步的方式能够比异步的方式更快的输出它的超单元的内容。
  • *双倍数据速率同步DRAM(Double Data-Rate Synchronous DRAM, DDR SDRAM)。DDR SDRAM是对SDRAM的一种增强,它通过使用两个时钟沿作为控制信号,从而使得DRAM的速度翻倍。不同类型的DDR SDRAM是用提高有效带宽的很小的预取缓冲区大小来划分:DDR(2位);DDR(4位);DDR(8位)
  • 视频RAM(Video RAM,VRAM)。主要被用在图形系统的帧缓冲区中。

现在大多数的服务器和桌面的系统都是采用DDR3 SDRAM构造的。

1.1.6 非易失性存储器

如果断电,DRAM和SRAM都会丢失它们的信息,从这个意义上说,它们都是*易失的(volatile)。另一个方面,非易失存储器(nonvolatile memory)*即使在关电后,仍然保存着它们的信息。

由于历史原因,虽然ROM中有的类型即可以读也可以写,但是它们整体上都被称为只读存储器(Read-only Memory)。ROM是以它们能够被重新编程的次数和对它们进行编程所用的机制来区分的。如PROM(类似芯片中的efuse),EEPROM(清除为0,对其操作是写入1)。

储存在ROM设备中的程序通常被称为固件(firmware)。当一个计算机系统通电以后,他会运行存储在ROM中的固件。如BIOS等

1.1.7 访问主存

数据流通过*总线(bus)的共享电子电路在处理器和DRAM主存之间来来回回。每次CPU和主存之间的数据传送都是通过一系列步骤来完成的。这些步骤被称为总线事务(bus transaction)读事务(read transaction)从主存传送数据到CPU。、写事务(write transaction)*从CPU传送数据到主存。

简单的理解总线就是一组并行的导线,可以传递数据,地址和控制信号。

不同的厂商提出不同的总线体系结构,作为产品差异化的一种方法。Intel系统使用北桥(northbridge)和南桥(southbridge)的芯片分别将CPU连接到内存和I/O设备。现代Intel处理器使用快速通道(QUickPath)互联。

1.2 磁盘存储

磁盘是为了应用和保存大量数据的存储设备,存储数据的数量级可以达到几百到几千兆字节。不过从磁盘上读取信息的时间为毫秒级,比DRAM读慢了10万倍,大约比SRAM慢了100万倍。

1.2.1 磁盘构造

磁盘是由*盘片(platter)构成的。每个盘片有两个面或者称为表面(surface),磁盘表面覆盖着磁性记录材料。盘片的中央有一个可以旋转的主轴(spindle),它使得盘片以固定的旋转速率(rotational rate)*旋转。磁盘通常包含一个或者多个这样的盘片,并封装在一个密闭的容器中。

对于一个典型的磁盘表面结构来说,每个磁盘的表面是由一组称为*磁道(track)的同心圆组成。每个磁道被划分成一组扇区(sector)。每个扇区包含相等数量的数据位(通常是512字节),这些数据编码在扇区上的磁性材料中。扇区之间由一些间隙(gap)*分隔开,这些间隙中不存储数据位。间隙存储用来识别扇区格式化位。

整个磁片加上旋转装置的通常被称为磁盘驱动器(disk drive)。通常简称为磁盘(disk),或者旋转磁盘(rotating disk),使其区别于固态硬盘SSD,SSD是没有移动部分的。

可以通过术语*柱面(sylinder)*来描述多个盘片驱动器的构造,这里,柱面是所有盘片变面上到主轴中心的距离相等的磁道的集合。

1.2.2 磁盘容量

一个磁盘上可以记录的最大位数称为它的最大容量,磁盘的容量是由以下技术因素决定。

  • 记录密度(recording density)(位/英寸):磁道一英寸的段中可以放入的位数。
  • 磁道密度(track density)(道/英寸):从盘片中心出发半径上一英寸的段内可以有的磁道数。
  • 面密度(areal density)(位/平方英寸):记录密度与磁道密度的乘积。
1.2.3 磁盘读写

磁盘用*读/写头(read/write head)来读写存储在磁性表面的位,而读写头连接到一个传动臂的一端。驱动器移动读/写头的机械运动称为寻道(seek)*。读/写头一般一致行动,在任意时刻,所有读/写头都位于同一个柱面上。

磁盘以扇区大小的块来读写数据。对扇区的访问时间又三个主要的部分:寻道时间(seek time), 旋转时间(rotational latency)和传送时间(transfer time)。

1.2.4 逻辑磁盘块

为了对操作系统隐藏磁盘系统的复杂性,通常会在磁盘中封装一个小的硬盘控制器,维护着逻辑号块和实际物理扇区之间的映射关系。当读取一个磁盘扇区的数据到主存,操作系统发送一个命令到磁盘控制器,让他读取某个逻辑块号。控制器上的固件通过查找,将一个逻辑块号转化成为一个(盘面,磁道,扇区)的三维数组。

磁盘控制器必须对磁盘进行格式化,然后才能在该磁盘上存储数据。格式化包括用标识扇区的信息填写扇区之间的间隙,标识出表面有故障的柱面并且不使用它们,以及在每个区中预留出一组柱面作为备用,如果区中一个或多个柱面在磁盘使用过程中坏掉了,就可以使用这些备用的柱面。因为存在着这些备用的柱面,所以磁盘制造商所说的格式化容量比最大容量要小。

1.2.5 连接I/O设备

如图形卡,鼠标,键盘和磁盘这些设备都是通过I/O总线连接到CPU的。一般来说I/O总线比系统总线和内存总线慢,但是它可以连接种类繁多的I/O设备。

1.2.6 访问磁盘

CPU使用一种称为*内存映射I/O(memory-mapped I/O)的技术来向I/O设备发射命令。对于CPU访问磁盘大致过程如下:

  1. CPU通过将命令,逻辑块号和目的内存地址写到与磁盘相关联的内存映射地址,发起一个磁盘读请求。
  2. 磁盘控制器读扇区,并执行道主存的DMA传送。
  3. 当DMA传送完成时,磁盘控制器通过中断的方式通知CPU

1.3 固态硬盘

固态硬盘(Solid State Disk, SSD)是一种基于闪存的存储技术。一个SSD封装由一个或多个闪存芯片和闪存翻译层(flash translation layer)组成。

对于SSD来说,读要比写要快。随机读和写的性能差别是由底层闪存基本属性决定。

SSD的随机写很慢,因为写入需要在一页的整个被擦除之后才能写这一页,擦除需要相对较长的时间。当写操作试图写入一个存在有用数据的块时,那么这个块中所带有用数据的页都必须被复制到一个新的块中,然后才能进行对页P的写。

1.4 存储技术的趋势

对于存储器技术的讨论,可以总价下面几个思想:

  • 不同的存储技术有着不同的价格和性能折中。
  • 不同存储技术的价格和性能有着截然不同的速率变化。

2. 局部性

一个编写良好的额计算机程序常常具有良好的*局部性(locality)。也就是,它们倾向于引用邻近于其他最近引用过的数据项,或者最近引用过的数据项本身。这种倾向性被称为局部性原理(principle of locality)*,是一个持久的概念,对硬件和软件系统的设计和性能都有着极大的影响。

局部性通常包含有两种不同的形式:*时间局部性(temporal locality)(被引用过一次的内存位置可能在不远的将来再次被引用)和空间局部性(spatial locality)*(如果一个内存位置被引用了一次,那么程序很可能在不远的将来引用到附近的一个内存位置)。

一般来说,有良好局部性的程序比局部性差的程序运行得更快。

2.1 对程序数据引用的局部性

例如下面的一段代码,可以看出,对于变量v,函数有很好的空间局部性,但是时间局部性很差,因为每个向量元素只被访问一次。对于循环结构代码来说,这个函数要么具有很好的空间局部性,要么有好的时间局部性。

1
2
3
4
5
6
7
int sumvec(int v[N])
{
int i, sum = 0;

for (i = 0; i < N; i++)
sum += v[i];
}

对于上面的C代码,按顺序访问每个元素的函数,具有步长为1的引用模式。一般将这种引用模式称为*顺序引用模式(sequential reference pattern)*。一个连续向量中,每隔K个元素进行访问,就称为步长为k的引用模式。一般来说,随着步长的增加,空间局部性下降。

2.2 取指令的局部性

因为程序指令是存放在内存中的,CPU必须取出这些指令,所以我们也能够评价一个程序关于取指令的局部性能。对于for循环来说,循环体里面的指令是按照连续的内存顺序执行的,因此循环有良好的空间局部性。因为循环体会被执行多次,所以它也有很好的时间局部性。

区别于程序数据的一个重要的属性是在运行的时候它是不能被修改的。当程序正在执行的时候,CPU只从内存中读出给它的指令。CPU很少会重写或者去修改这些指令。

2.3 局部性小结

对于量化评价程序中局部性的一些简单原则:

  • 对于重复引用相同变量的程序有良好的时间局部性。
  • 对于具有步长为k的引用模式的程序,步长越小,空间局部性越好。
  • 对于取指令来说,循环有好的时间和空间局部性。循环体越小,循环迭代次数越多,局部性越好。

3. 存储器层次结构

  • 存储技术:不同存储技术的访问时间差异很大。速度较快的技术每字节的成本要比速度较慢的技术高,而且容量较小。CPU和主存之间的速度差异在增大。
  • 计算机软件:一个编写良好的程序倾向于展示出良好的局部性。

计算机通过一种称为*存储器层次结构(memory hierarchy)*的组织方法使得硬件和软件的这些基本属性互相补充的十分完美。存储器的层次结构由块到慢如下形式: 寄存器 --> L1高速缓存(SRAM) --> L2高速缓存(SRAM) --> L3高速缓存(SRAM) --> 主存(DRAM) --> 本地二级缓存(本地磁盘) --> 远程二级存储(分布式文件系统,web服务器)

3.1 存储层次结构中的缓存

一般来说,*高速缓存(cache)*是一个小而且快速的存储设备,它作为存储在更大,也更慢的设备中的数据对象的缓冲区。

存储器层次结构的中心思想是,对于每个位于k层的更快的存储设备作为位于k+1层的更大更慢的存储设备的缓存。换句话说,层次结构中的每一层都缓存来自较低一层的数据对象。例如,主存作为本地磁盘的缓存。

一般来说第k层更小,更快,更昂贵的设备缓存着第k+1层块的一个子集,数据以块为大小传输在层与层之间复制。

数据总是以块大小为*传送单元(transfer unit)*在第k层和第k+1层之间来回复制的。虽然在层次结构中任何一对相邻的层次之间块大小是固定的,但是其他层次对之间可以有不同的块大小。一般来说,层次结构中较低层(离CPU较远)的设备的访问时间较长,因此为了补偿这些较长的访问时间,倾向于使用较大的块。

3.1.1 缓存命中

当程序需要第k+1层的某个数据对象d的时候,它首先在当前存储在第k层的一个块中查找d。如果d刚好缓存在第k层中,那么就是我们所说的*缓存命中(cache hit)*。这时程序直接从第k层中读取d。

3.1.2 缓存步命中

当第k层中没有缓存数据对象d的时候,那么就是我们所说的*缓存不命中(cache miss)*。当发生缓存不命中的时候,第k层的缓存从第k+1层缓存中取出包含d的那个块,如果第k层的缓存已经满了,可能就会覆盖现存的一个块。

这里覆盖一个现存的块的过程称为*替换(replacing)或者驱逐(evicting)这个块。被驱逐的这个块有时被称为牺牲块(victim block)。决定应该替换那个块是由缓存的替换策略(replacement policy)*来控制的。可以随机替换或者选择最后被访问的时间距离现在最远的块。

3.1.3 缓存不命中的种类

当第k层的缓存是空的,那么对任何数据对象的访问都不会命中。一个空的缓存被称为*冷缓存(cold cache),此类不命中称为强制不命中(compulsory miss)或者冷不命中(cold miss)*。这种缓存不命中的形式通常是短暂的时间,不会在稳定状态下出现。

当缓存按照某种规则替换k+1层的块,就有可能出现*冲突不命中(conflict miss)*的情况。例如制定某种规则,缓存K层分为4个块,每个块只会替换K+1层中编号对4取余相等的块,例如,当需要k+1层中7号内存块中的数据会将其复制到编号为3的k层数据块中。但是这种情况如果程序只需要编号为3和7的k+1层中的数据块就需要反复的复制3和7到k层缓存中,即使k层缓存总共可以容纳4个块。

当程序需要反复访问一个数组中的元素。同时这数组的大小超过缓存的大小的时候,缓存就会出现*容量不命中(capacity miss)*。简单的说就是缓存太小,不能处理这个数组。

3.1.4 缓存管理

存储器层次结构的本质是,每一层存储设备都是较低一层的缓存。在每一层上,存在某种形式的逻辑管理缓存。例如将缓存划分成块,在不同的层之间传送块,判定是命中还是不命中并处理它们。管理缓存的逻辑可以是硬件、软件,或是两者的结合。

3.2 存储层次结构概念小结

基于缓存的存储器层次结构行之有效,是因为较慢的存储设备比较快的存储设备更便宜,同时程序倾向于展示局部性。

类型 缓存什么 被缓存在何处 延迟(周期数) 由谁管理
CPU寄存器 4字节或者8字节字 芯片上的CPU寄存器 0 编译器
TLB 地址翻译 芯片上的TLB 0 硬件MMU
L1高速缓存 64字节块 芯片上的L1高速缓存 4 硬件
L2高速缓存 64字节块 芯片上的L2高速缓存 10 硬件
L3高速缓存 64字节块 芯片上的L3高速缓存 50 硬件
虚拟内存 4KB页 主存 200 硬件+OS
缓冲区缓存 部分文件 主存 200 OS
磁盘缓存 磁盘扇区 磁盘控制器 100 000 控制器固件
网络缓存 部分文件 本地磁盘 10 000 000 NFS客户端
浏览器缓存 web页 本地磁盘 10 000 000 Web浏览器
web缓存 web页 远程服务器磁盘 1 000 000 000 Web代理服务器

4. 高速缓存存储器

在这一节的讨论中,我们会假设一个简单的存储器层次结构,CPU和主存储器之间只有一个L1高速缓存。

4.1 通用高速缓存存储器组织结构

假设一个计算机系统,其存储器控制器地址有m位,形成M=2^m个不同的地址。同时这样的一个机器的高速缓存被组织成一个有S=2^s个高速缓存组(cache set)的数组。每个组包含E个高速缓存行(cache line)。每个行是由一个B=2^b字节的数据块(block)组成,其中数据块中有一个有效位指明这行数据是否包含有意义的信息。还存在t=m-(b+s)个标记位,它们唯一地标识出存储在这个高速缓存行中的块。

一般来说,高速缓存的结构可以通过元组(S,E,B,m)来描述。高速缓存的大小C通常是指所有块的大小的和。标记位和有效位不包含在里面。因此,C=S*E*B

4.2 直接映射高速缓存

根据每个组的高速缓存行数E,高速缓存被分成不同的类别。对于每个组只有一行的高速缓存被称为直接映射高速缓存。

对于一个直接映射高速缓存的系统,我们假设它有一个CPU、一个L1高速缓存和一个主存。当CPU执行一条读内存字的w指令的时候,它向L1高速缓存请求这个字。如果L1高速缓存中有w的一个缓存副本,那么就得到L1高速缓存命中,高速缓存就会很快的抽取这个w,并将它返回给CPU。否则就会发生缓存不命中,当L1高速缓存向主存储请求包含w的块的一个副本时,CPU必须等待。当被请求的块最终从内存到达时,L1高速缓存将这个块存放在它的一个高速缓存行里,从被存储的块中抽取字w,然后将它返回给CPU。高速缓存确定一个请求是否命中,然后抽取被请求的字的过程分为三步:1)组选择,2)行匹配,3)字抽取。

4.2.1 直接映射高速缓存系统的组选择

高速缓存从w地址抽取出s个组的索引位。这些位被解释成一个对应于一个组的无符号数,或者简单的解释,当我们把高速缓存看成一个关于组的一维数组,那么这些组索引位就是一个到这个数组的索引。

4.2.2 直接映射高速缓存系统的行匹配

4.2.1 中通过地址位获得了组索引,这里将确定是否有字w的一个副本存储在组i包含的一个高速缓存行中。这里因为每个组只有一行,当且仅当设置了有效位,而且高速缓存行中的标记与w的地址中的标记匹配时,这一行中包含w的一个副本。

只有当高速缓存行中的标记位与地址中的标记位相匹配,才能说缓存命中,同时如果有效位没有设置,或者标记不相匹配,那么我们就得到了一个缓存不命中。

4.2.3 直接映射高速缓存系统的字抽取

一旦缓存命中,我们知道w就在这个块中的某个地方。最后一步确定所需要的字在块中是从哪里开始的。可以将块偏移理解为数组索引。

4.2.4 直接映射高速缓存系统不命中时的行替换

如果缓存不命中,那么它需要从存储器层次结构的下一层取出被请求的块,然后将新的块存储在组索引位指示的组中的一个高速缓存中。一般而言,如果组中都是有效的高速缓存行,那么必须要驱逐一个现存的行。对于直接映射高速缓存来说,每个组只包含有一行,替换策略为:用新取出的行替换当前的行。

4.3 组相联高速缓存

组相联高速缓存允许每个组保存有多于一个的高速缓存行。

4.4 全相连高速缓存

全相连高速缓存是由一个包含所有高速缓存行的组(即 E=C/B) 组成。简单的理解只有一个组。

因为高速缓存电路必须并行的搜索许多相匹配的标记,构造一个又大又块的相关联高速缓存十分困难,而且昂贵。因此,全相联高速缓存只适合做小的高速缓存,例如虚拟内存系统中的翻译备用缓冲器。

4.5 有关写的问题

前面描写的高速缓存关于读的操作可以概括为:首先,在高速缓存中查找所需要的字w的副本。如果命中,立即将w返回给CPU。如果不命中,从存储器的层次结构中较低的层中取出包含w的块,将这个块存储到某个高速缓存中(可能会驱逐一个有效的行),然后返回字w。

对于写的情况则比较复杂。假设我们要写一个已经缓存了的字w(写命中,write hit) 方法如下:

  1. 直写(write-through),就是立即将w的高速缓存块写回到紧接着的低一层中。这种方法会导致每次写回都要引起总线流量。
  2. 写回(write-back),尽可能的推迟更新,只有当替换算法要驱逐这个更新过的块的时候,才把它写到紧接着的低一层中。可以显著的减少总线流量。

当我们要写回的数据不在缓存中的时候,称为写不命中,具体方法有:

  1. 写分配(write-allocate),加载相应的低一层的块到高速缓存中,然后更新这个高速缓存。
  2. 非写分配(not-write-allocate),避开高速缓存,直接把这个字写到底一层中。

4.6 实际高速缓存层次结构分析

对于高速缓存,只保存指令的高速缓存称为i-cache,只保存程序数据的高速缓存称为d-cache。既保存指令又保存数据的高速缓存称为统一的高速缓存。通常会针对不同的访问模式来优化这两个高速缓存,它们可以有不同的块大小,相联度和容量。

4.7 高速缓存参数的性能影响

有许多指标来衡量高速缓存的性能。

  • 不命中率(miss rate)。在一个程序执行或程序的一部分执行期间,内存引用不命中的比率。
  • 命中率(hit rate)。命中内存的比率。
  • 命中时间(hit time)。从高速缓存传送一个字到CPU所需要的时间。包括组选择、行确认和字选择时间。
  • 不命中处罚(miss penalty)。由于不命中所需要的额外时间。

5. 编写高速缓存友好的代码

编写高速缓存友好的代码的基本方法如下:

  1. 让最常见的情况运行的快,程序通常把大部分时间都花在少量的核心函数上面,而这些函数通常把大部分时间都花在少量的循环上面。所以要把注意力集中在核心函数里的循环上,而忽略其他部分。
  2. 尽量减小每个循环内部的缓存不命中数量。总的说来就是1)对局部变量的反复引用是好的,因为编译器能够将它们缓存在寄存器文件中;2)步长为1的引用模式是好的,因为存储器的层次结构中所有层次上的缓存都是将数据存储为连续的块。

较高的不命中率会对运行时间有显著的影响。

6. 高速缓存对程序性能的影响

这里将通过研究高速缓存对运行在实际机器上的程序性能的影响,综合我们对存储层次结构的讨论。

6.1 存储器山

一个程序从存储系统中读取数据的速率称为*读吞吐量(read throughput),或者称为读带宽(read bandwidth)*。如果一个程序在s秒的时间段内读n个字节,那么这段时间内的读吞吐就等于n/s。

总的来说,存储器系统的性能不是一个数字就能描述的。相反,他是一座时间和空间局部性的山,这里我们称之为存储山。