处理器体系结构-||
存储器的层次结构
存储器系统是由不同容量,成本和访问时间的存储设备组成的层次结构,在这个层次结构中:CPU寄存器保存最常用的数据,靠近CPU的小的,快速的高速缓存存储器作为相对慢速的主存储器中数据和指令的缓冲区域,主存又作为容量较大,速度较慢的磁盘上数据的缓冲区域,而磁盘又可以作为网络中其他机器上数据的缓冲区域
存储器层次结构对应用程序的性能有着巨大的影响,
假如数据存储在寄存器中,那么0个始终周期就能访问到它们 如果存储在高速缓存中,则需要4~75个周期 如果存储在磁盘上,则需要约几千万个周期
之所以这样的差异,不同存储器使用的存储技术不同,基本的存储技术包括SRAM存储器,DRAM存储器,ROM存储器,旋转硬盘以及固态硬盘
随机访问存储器
静态(SRAM) 动态(DRAM)
静态存储器比动态存储器更快,但也更贵,SRAM常用来作为高速缓存存储器,DRAM常用来作为主存以及图形系统的帧缓冲区
SRAM
SRAM是存储技术中随机访问存储器的一种
SRAM将每个位存储在一个双稳态存储器单元中,每个单元用一个六晶体管电路来实现,所谓双稳态,指的是该存储器单元可以无限期地保持在两个不同的电压配置之一
只要有电,SRAM存储单元就会永远保存它的值。即使有干扰扰乱电压,当干扰消除时,电路就会恢复到稳定值
CPU的一级缓冲,二级缓冲用sram
DRAM
动态RAM 保留数据的时间很短,速度也比SRAM慢 dram将每个位存储为对一个电容的充电,存储器单元由一个电容和一个访问晶体管组成,与SRAM不同,DRAM存储器单元对干扰非常敏感,而且受到干扰后永远不会恢复
有很多原因会导致DRAM单元漏电,使得它在10~100MS内失去电荷。但由于计算机运行的时钟周期是以纳秒来衡量的,因此相对来说DRAM单元电荷保持的时间还是比较长的,由于这个原因,内存系统会周期性地通过读出重写来刷新内存的每一位,有些系统还使用纠错码来发现并纠正一个字中任何单个的错误位
传统DRAM
dram分为许多种类,DRAM芯片的单元被分为d个超单元,每个超单元由W个DRAM单元组成,所有的超单元被组织成一个rXc的长方形阵列(rc=d)每个超单元有形如(i,j)的地址,其中i表示行,j表示列
前面展示的是16X8的DRAM芯片的组织,有d=16个超单元 每个超单元有w=8位,r=4行,c=4列,信息通过引脚的外部连接器流入和流出芯片,每个引脚携带1个1位信号、 图中展示了两组引脚, 8个data引脚用于传送一个字节到芯片或从芯片传出一个字节 2个addr引脚,用于携带行和列单元地址 每个DRAM芯片被连接到某个称为内存控制器的电路
每个电路可以一次传送w位到每个DRAM芯片或一次从每个DRAM芯片传出w位 如果要读超单元(i,j)的内容,内容控制器首先将行地址i发送到dram,然后是列地址j。DRAM把超单元(i,j)的内容发回给控制器作为响应。行地址i称为RAS(行访问选通脉冲)请求,列地址j称CAS请求(列访问选通脉冲)
传统DRAM
例如要从前图中16x8的DRAM中读出超单元(2,1),内存控制器发送行地址2,随后DRAM将地址为2的整行都复制到一个内部行缓冲区中。接下来内存控制器发送列地址1,DRAM就将列地址为1的8位数据发送到内存控制器
之所以需要将行地址和列地址分别传送,是因为DRAM是以二维阵列的形式组织的,这样做是为了减少引脚的数量。如果上面的128位DRAM被组织成16个超单元的线性数组,地址为0~15 ,那么芯片将会需要4个引脚
在实际的计算机体系结构中,DRAM芯片被封装在内存模块中并插到主板的扩展槽上。内存模块的基本思想是使用多个dram芯片,每个芯片的每个超单元存储主存中的一个字节,并用相应超单元地址为(i,j)的8个超单元来表示主存中字节地址A处的64位字
例如要取出内存地址 A处的一个字,内存控制器首先将A转换成一个超单元地址(i,j)并将它发送到内存模块,内存模块再将i和j广播到每个DRAM。作为相应。每个DRAM输出它的(i,j)超单元的8位内容。模块中的电路收集这些输出,并把它们合并成一个64位字,返回给内存控制器
磁盘存储
前面介绍的随机访问存储器必须在有电的时候才能存取数据 磁盘能够存储大量的数据,不过,从磁盘上都信息的时间为毫秒级,比从DRAM读数据慢了10万倍 比从SRAM读数据慢100万倍
磁盘构造
磁盘由盘片(platter)和可以旋转的主轴(spindle)构成。每个盘片都有两个表面。上面覆盖着磁性记录材料。主轴使盘片能以固定的旋转速率旋转,通常为5400~15000转每分钟(“转每分钟”也作为单位,RPM)
磁盘盘片的表面由一组磁道(track)的同心圆组成。每条磁道由存储数据的扇区(sector)和标识扇区的间隙gap构成,其中每个扇区包含相等的数据位,而间隙仅用于标识扇区的格式化位,并不存储数据
磁盘由一个或多个叠放的盘片组成,并非封装在一个密封的包装里。整个装置称为磁盘驱动器。简称磁盘 通常磁盘又被称为旋转硬盘或者机械硬盘
KMG表示的含义
SRAM,DRAM K=
2
1
0
2^10
210 M=
2
2
0
2^20
220 G=
2
3
0
2^30
230 T=
2
4
0
2^40
240
IO设备 K=
1
0
3
10^3
103 M=
1
0
6
10^6
106 G=
1
0
9
10^9
109 T=
1
0
1
2
10^12
1012
磁盘容量
- 记录密度
位/英寸 磁道一英寸的段中可以放入的位 - 磁道密度
道/英寸从盘中心出发半径上一英寸的段内可以有的磁道数 - 面密度
位/平方英寸记录密度与磁道密度的乘积
磁盘操作
磁盘通过读写头来读写存储在磁性表面的位 而读写头连接到一个传动臂一端。通过沿着半径方向移动传动臂,驱动器可以将读写头定位在盘片的任何磁道上,在这个过程中读写头垂直排列,一致行动
磁盘读写数据的时间主要受以下因素影响:
- 寻道时间:读写数据时,传动臂首先需要将读写头定位到包含目标扇区的磁道上,这个移动过程所花的时间称为寻道时间。寻道时间主要依赖于读写头的位置和传动臂在盘面上移动的速度。经过测量,现代驱动器中的平均寻道时间通
常为3~9ms。一次寻道的最大时间可高达20ms。 - 旋转时间:当读写头移动到了目标磁道上,就需要等待目标扇区的第一个位旋转到读写头下。这个过程所花的时间称为旋转时间。旋转时间主要依赖于读写头到达目标磁道时目标扇区的位置以及磁盘的旋转速度。最坏的情况下读写头必须等待磁盘旋转一圈。
- 传送时间:当目标扇区的第一个位位于读写头下时,驱动器开始读或写该扇区的内容。一个扇区的传送时间依赖于旋转速度和每条磁道的扇区数目。
逻辑磁盘块
为了对操作系统隐藏磁盘构造的复杂性,在现代磁盘的结构中,通常将盘面上的各个记录区呈现为一个B个扇区大小的逻辑块序列,编号为 0,1… B-1 并使用磁盘控制器来维护逻辑块号和实际磁盘扇区之间的映射关系
当操作系统需要执行一个读写操作,例如读一个磁盘扇区的数据到主存时,操作系统会将命令发送到磁盘控制器,让它读某个逻辑块号。控制器上的固件执行一个快速表查找,将一个逻辑块号翻译成一个三元组:(盘面,磁道,扇区)
这个三元组唯一表示了对应的物理扇区,随后控制器上的硬件会解释这个三元组,将读写头移动到适当的柱面,等待扇区移动到读写头下,随后将读写头感知到的位放到控制器上的一个缓冲区中,然后将它们复制到主存中
固态硬盘
与磁盘不同,固态硬盘(ssd)是一种基于闪存的存储技术 一个ssd由一个或多个闪存芯片以及闪存翻译层组成。闪存芯片取代了磁盘中机械驱动器。闪存翻译层则扮演与磁盘控制器相同的角色,主要将操作系统对逻辑块的请求翻译成对物理设备的访问
在SSD中,一个闪存由B个块组成,每个块又由P页组成。 通常 页的大小为512字节~4KB 块由32~128页组成 块的大小为16KB~512KB
数据在SSD中是以页为单位读写的。 在写入数据之前,需要对一页所属的块进行擦除操作,所谓擦除,指的是将该块中所有位,置为1,而写入的操作实际上就是将部分位,置为0
当擦除次数过多,块会因为磨损而损坏。 为了减少这种磨损带来的影响,闪存翻译使用一种平均磨损算法,来擦除平均到各个块上
局部性
所谓局部性,指的是每次引用数据时倾向与引用最近引用过的数据项邻近的数据项,或最近引用过的数据项本身。一个编写良好的程序通常要求具有好的局部性。 局部性
- 时间局部性
被引用过的一次数据项会在不远的将来再次被多次引用 - 空间局部性
如果某个数据在某个位置被引用了一次,那么在不远的将来将引用它附近内存位置
int sumvec(int v[N]){
int i,sum=0;
for(i = 0;i < N; i++)sum += v[i];
return sum;
}
变量sum在循环中都被引用一次,因此对于sum来说,该程序具有好的时间局部性,对于向量v来说,v中的元素被一个一个地读取,因此该程序具有好的空间局部性。综上,我们说该函数具有良好的局部性
int sumarrayrows(int a[M][N]){
int i,j,sum=0;
for(i=0;i<M;i++){
for(j=0;j<N;j++){
sum += a[i][j];
}
}
return sum;
}
双重嵌套循环按行优先顺序读数组中的元素,由于二维数组 也是按照行优先顺序存储的,因此该函数也具有良好的空间局部性。但由于数组中的每一个元素都只被使用了一次,因此该函数不具备良好的时间局部性
如果改为了列优先
int sumarrayrows(int a[M][N]){
int i,j,sum=0;
for(j=0;j<N;j++){
for(i=0;i<N;i++){
sum += a[i][j];
}
}
return sum;
}
步长对于局部性的影响
在前面的两个例子中,程序按一个接一个的顺序访问数组,我们称这种访问顺序为“步长为1的引用模式”,或称为“顺序引用模式”。在一些循环中,会对一个连续向量每隔 k 个元素进行访问,这时称其为“步长为 k 的引用模式”。
步长为 1 的引用模式是空间局部性较好的引用模式 步长越大,空间局部性就越差。
例如在最后一个例子中,虽然看上去也是一个接一个地引 用数组中的元素,但由于引用顺序没有按照数组的行优先存储顺序来引用,所以这实际上是个步长为 N 的引用模式。
存储器的层次结构
高速缓存(cache)是作为更大、更慢的存储设备的一个缓冲区域,它相对而言更小、更快速。存储器层次结构的中心思想是,对于每个 k,位于 k 层的更快更小的存储设备作为位于 k + 1 层更大更慢的存储设备的缓存
在存储器层次结构中,第 k + 1 层的存储器被划分成连续的数据对象组块(chunk), 称为块(block)。每个块都有一个唯一的地址或名字。通常块的大小是固定的,但也可能是可变的(例如存储在 Web 服务器上的远程 HTML 文件)。在第 k 层中,存储器被划分成较少的块的集合,其中每个块的大小与 k + 1 层中的块的大小相同。这样,数据就以块大小为传送单元在第 k 层和 k + 1 层之间来回复制。
缓存命中
当程序需要第 k + 1 层的某个数据对象 d 时,它首先在当前存储在第 k 层的一个块中查找 d。如果 d 刚好缓存在第 k 层中,那就称为缓存命中,这时程序直接从第 k层读取 d。由于第 k 层相对于 k + 1 层来说是更块的存储设备,因此读取速度比从第 k + 1 层读取更快。
如果程序在第 k 层中找不到缓存数据对象 d,那么就是缓存不命中。当缓存不命中时,就需要从第 k 层中取出包含 d 的那个块,此时如果 k 层的缓存已经满了,那么就可能会覆盖现存的一个块。
覆盖一个现存块的过程称为替换或驱逐这个块。决定该替换哪个块是由缓存的替换策略(replacement policy)控制的。 例如,随机替换策略会随机替换一个块;而LRU 替换策略会选择替换最后被访问的时间距现在最远的块。
缓存不命中的种类
定义:如果一个缓存是空的,那就称它为冷缓存(cold cache)。(一开始的情况) 情况:冷缓存对任何数据对象的访问都会不命中,这类不命中称为强制性不命中(compulsory miss)或冷不命中(cold miss)。冷不命中通常是短暂的事件,不会在存储器的稳定状态中出现。 如果发生了不命中,那么第 k 层的缓存就必须执行某个放置策略(placementpolicy),来确定把从第 k + 1 层中取出来的块放在哪里。最灵活的替换策略是允许来自第 k + 1 层的任何块放在第 k 层的任何块中。
实际上,靠近 CPU 的缓存是通过硬件实现的,实现随机放置块的代价很高,因此硬件缓存通常使用更严格的放置策略。 这个策略将第 k + 1 层的某个块限制放置在第k 层块的一个小的子集中(有时只是一个块)。例如可以限制第 k + 1 层的块 i 必须放置在第 k 层的块 (i mod 4) 中。这样第 k + 1 层的块0、4、8 和 12 都会被映射到第 k 层的块 0 中。
使用这种放置策略可能会造成冲突不命中(conflict miss)。在这种情况下,部分对象会被映射到同一个缓存块,导致缓存一直不命中。 可能会发生抖动现象
解决抖动
假设:我们接下来假设 CPU 和主存之间只有一个高速缓存。
通用高速缓存存储器组织结构
考虑一个计算机系统,其中每个存储器地址有 m 位,形成 M =
2
m
2^m
2m 个不同的地址。 这样的一个高速缓存器被组织成一个有 S =
2
s
2^s
2s 个高速缓存组的数组。每个组包含E 个高速缓存行。每个行由一个 B = 2b 字节的数据块组成。其中还包括有一个有效位,用于指明这个行是否包含有意义的信息。另外有 t = m (b + s) 个标记位用于唯一地表示存储在这个高速缓存行中的块。
cache结构
(
S
,
E
,
B
,
m
)
(S,E,B,m)
(S,E,B,m)
一般而言,高速缓存结构可以用一个四元组
(
S
,
E
,
B
,
m
)
(S, E, B, m)
(S,E,B,m) 来描述。高速缓存的大小 C 指的是所有块的大小的和,标记位和有效位不包括在内,因此
C
=
S
×
E
×
B
C = S × E × B
C=S×E×B。
通用高速缓存存储器组织结构
当一条加载指令指示 CPU 从主存地址 A 中读一个字时,它将地址 A 发送到高速缓存。如果高速缓存正保存着地址 A 处那个字的副本,它就立即将那个字发回给CPU。可以发现,在这个过程中,高速缓存需要知道它自己是否包含地址 A 处那个 字的副本。
实际上,高速缓存是通过检查地址位来寻找所请求的字的。在高速缓存结构元组中,S 和 B 将 m 个地址分为了标记、组索引和块偏移三个字段。地址 A 中 s 个组索引位是一个到 S 个组的数组的索引,是一个无符号整数。组索引位告诉我们这个字必须存储在哪个组中。 一旦我们知道了这个字要被放在哪个组中,A 中 t 个标记位就告诉我们这个组中的哪一行包含这个字。当且仅当设置了有效位并且该行的标记位与地址 A 中的标记位相匹配时,组中的这一行才包含这个字。一旦我们在由组索引标识的组中定位了由标号所标识的行,那么 b 个块偏移位给出了在 B 个字节的数据块中的字偏移。
直接映射高速缓存
根据每个组的高速缓存行数 E,高速缓存被分为不同的类。每个组只有一行的高速缓存被称为直接映射高速缓存。 当 CPU 执行一条读内存字 w 的指令时,它向 L1 高速缓存请求这个字。如果 L1 中 有 w 的一个缓存的副本,那么缓存命中,L1 将抽取出 w 并将它返回给 CPU。否则缓存不命中,这时 L1 就要向主存请求包含 w 的块的副本。当被请求的块到达时,L1 就将这个块存放在它的一个高速缓存行里,从被存储的块中抽取出字 w,然后将它返回给 CPU。
在上述过程中,高速缓存的操作分为三步: 1)组选择; 2)行匹配; 3)字抽取。 其中组选择较为简单,高速缓存从 w 的地址中抽取出 s 个组索引位。这些位被解释成一个对于于一个组号的无符号整数。 假设已经确定了某个组 i 接下来要确定是否有字 w 的一个副本存储在某个组 i 包含的一个高速缓存行中。由于在直接映射高速缓存中,每个组只有一行。因此当且仅当设置了有效位,而且高速缓存行中的标记 w 与地址中的标记相匹配时,这一行中包含 w 的一个副本。 接下来我们要确定所需要的字在块中是从哪里开始的,这通过块偏移位就可看出。 例如块偏移位是 1002,则表明 w 的副本是从块的字节 4 开始的(假设字长为 4 字节)。
如果发生了缓存不命中,那么就需要从存储器层次结构的下一层中取出所请求的块并存储在组索引位指示的组中的一个高速缓存行中。如果组中都是有效高速缓存行,那么就用新取出的行替换当前的行。
部分图片引自https://www.bilibili.com/video/BV1cD4y1D7uR?p=42
组相联高速缓存
1<E<C/B
- 组相联cache的每个set允许包含多个cache line
- 每个set包含两个cache line(2路组联)
if (valid=0) 没有空行怎么办? else(){ 1.随机替换 2.最不常使用的LFU }
全相联
cache只有一个set,也就是说一个set包含了所有的cache line 全相联只有一个set。默认总是选择set 0 不需要进行组选择 全相联只适合做容量小的高速缓存 example 虚拟内存系统中的TLB可以使用这种结构
写命中和写不命中
- 写命中
- 写穿透 写cache同时写内存,内存的数据永远是新的
- 写回 CPU只写cache,不写内存,替换算法要驱逐这个更新的块时写回 增加一个修改位
- 写不命中
- 写分配 目标数据所在的块从内存加载到cache中,再往cache中写
- 写不分配 直接把要写的内容写到内存里
写分配与写回搭配使用,写不分配与写穿透搭配使用 cache既保存数据,也保存指令 i-cache 只保存指令 d-cache 只保存数据
采用独立的i-cache和d-cache的好处是可以同时读指令和数据,还能确保数据访问不会与指令和指令访问形成冲突不命中 容量较大的cache可以提高命中率,但是可能增加命中时间 数据块的大小也会影响cache的性能 较大的块能够利用程序中可能存在空间局部性,帮助提高命中率,但是块越大,cache行数越小,对于时间局部性好的程序会收到损害。发生不命中的时候,较大的块的处罚也比较大,块越大,传送时间越长
cacheline越多,由于冲突不命中导致的抖动现象发生的几率就越低
相联度越高,实现的复杂度越高,访问速度就很难再提高了
最终相联度如何选择需要在命中时间和不命中处罚之间做一个折中的处理
L1 cache 与L2 cache之间用写穿透的多一些 越往下走,写回策略用的多一些
链接的相关内容
Linking 链接是将各种代码和数据收集并组合成一个文件的过程
链接器是如何利用库文件来解析引用的
- (构造大型程序)
- 避免一些危险的编程错误)
- (理解语言的作用域是如何实现的)
- (理解其他重要的系统概念)
- (更好的利用共享库)
linux>gcc -Og -o prog main,c sum,c
- 预处理任务
linux>cpp =o main.i main.c
linux>gcc -E -o main.i main.c
main.c main.i(ASCII的中间文件)
- 编译
linux>cc -S -o main.s main.i
cc:c编译器
-S 只编译
gcc -S -o main,s main.i
- 汇编
linux>as -o main.s main.o
as汇编器
main.o sum.o
- 链接
main.o sum.o crt.i crt1.o crtbeginT.o crtend.o crtn.o
linux> ld -static -o prog main,o sum.o
-static 表示采用静态链接的方法 通过这条命令我们可以实现手动链接的操作
可执行文件的运行是通过shell调用操作系统中的加载器函数来实现的,加载器将可执行文件prog中的代码和数据复制到内存中
然后将CPU的控制权转移到prog的程序开头
可重定位文件
linux>ld -static -o prog main.o sum.o
将文件翻译成可执行文件
linux>gcc -c main.c
-c 编译和汇编,但是不链接
WC选项查看有多少个字节
linux>wc -c main.o
-c 查看文件包含多少个字节
可重定位文件 ELF:executable and linkable format 可执行可链接格式
readelf -h main.o 查看elfheader中的内容 -h 显示header信息 readelf工具
魔数:确认文件类型 ELF长度64字节
三种文件类型
- 可执行文件
- 共享文件
- 可重定位文件
可重定向文件内容
描述不同section属性的表
linux> readelf -S main.o
-S 打印整个表 除了表中的第一项,每个表项对应着一个section 根据图中的信息,可以看到整个elf文件一共包含12个section 其中offset表示每个 section的起始位置,size表示section的大小
objdump -s -d main.o 反汇编查看
.text section
- .text section 之后是.data section
- .data section 存放已初始化的全局变量和静态变量的值(小端存放)
- .bss section 对于未初始化的全局变量和静态变量会存放在.bss section 中,以及被初始化为0的全局变量和静态变量 .bss section 只是一个占位符,不占用空间
- .rodata section 存放只读数据
printf语句中的格式串和switch语句中的跳转表就是存在这个区域内 .rodata section 保存了需要打印的字符串
符号和符号表
每个可重定位文件都有一个符号表,这个符号表包含该模块定义和引用的符号信息
其他Sections .symtab
查看符号表的内容readelf
readelf -s main.o
索引值与具体section的对应关系可以查看 section header table来确定
value表示函数相对于.text section 起始位置的偏移量
名称修饰,主要为了防止静态变量的名字冲突
局部变量在运行时被栈管理,与链接器无关
三种符号
- 全局符号
模块定义,同时能被其他模块引用的全局符号 main.c中定义的函数func以及全局变量count和value - 外部符号.
被其他模块定义,同时被该模块引用的全局符号 - 局部符号
只能被该模块定义和引用的局部符号 区别局部符号和全局符号的关键就是static 属性 任何带有static属性声明的全局变量或者函数都是模块私有的 任何不带static属性声明的全局变量和函数都是公共的,可以被其他模块访问 尽可能用static 属性来保护你的变量和函数是一种很好的编程习惯
|