-
存储器层次结构基于缓存的存储器层次结构,通过让第k层的存储器为第k+1层存储器作缓存,提升数据访问效率。 因此,在程序设计过程中,应将最经常使用的数据尽可能多的存储于高速缓存中。存储器层次结构如下:
-
- 局部性
存储器的存储策略对程序的执行效率有着很重要的影响,其中一个重要属性:局部性。
局部性两种形式:时间局部性和空间局部性。前者指 被引用过的存储器位置很可能在不久再被多次引用。后者指如果一个存储器位置为引用一次,其附近的存储位置也将在不久不引用。硬件层中的高速缓存正是利用这样一种特性,缓存主存中的数据,从而大大提升对主存的访问速度。(对高速函数的访问速度,远远大于对主存的直接访问速度)
为了利用局部性,循环常常采用步长为1的引用。例:
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[M][N];
return sum;
}
(a) int sumarraycols(int a[M][N])
{
int i, j, sum =0;
for(i = 0; i < N i++)
for(j=0; j < M; j++)
sum += a[M][N];
return sum;
}
(b)在数组进行行先存储策略的机器中,(a)对各数据的引用为步长为1的引用,(b)则为步长为N的引用,所以(b)空间局部性较差。
程序员应尽可能编写局部性较好的程序,这样有助于将尽可能多的数据放在高速缓存中,而不是从主存中读取,从而提高CPU的读取速度。
- 高速缓存的利用
例如:一个块16个字节,两个组组成的直接高速缓存(每组一行数据块)中,此时高速缓存大小为16×2=32个字节
在执行如下程序时
floal x[8], y[8];
float dotprod(float x[8], float y[8])
{
float sum = 0.0;
int i;
for(i = 0; i < 8; i++)
sum += x[i]*y[i];
return sum;
}
假设sizeof(float) = 4. x被加载在 0~31的连续地址空间中,y 在32~63的空间。则x,y数据对高速缓存有如下映射关系:
如此可看出,在第一次引用x[0]时x[1]~x[3]被同时缓存在 高速缓存的组0,同时引用y[0]时y[1]~y[3]同样缓存在组0中,将x[0~3]的缓存 驱逐,下次访问x[1]时,同样驱逐y[0~3]的缓存...由此可看出对x,y的访问,高速缓存反复加载和驱逐相同的组,导致每次访问均不命中,性能低下。访问命中率为 0%
而将程序做如下改动:
floal x[12],y[12];
float dotprod(float x[12], float y[12])
{
float sum = 0.0;
int i;
for(i = 0; i < 8; i++)
sum += x[i]*y[i];
return sum;
}
x,y分配空间增大后,x,y数据对高速缓存有如下映射关系:
此时,对x,y引用时,高速缓存 加载与数据命中与否满足如下关系
需要加载到缓存 | 命中 | 对应缓存组号 | |
x[0] | 是 | 否 | 0 |
y[0] | 是 | 否 | 1 |
x[1..3] | 否 | 是 | 0 |
y[1..3] | 否 | 是 | 1 |
x[4] | 是 | 否 | 1 |
y[4] | 是 | 否 | 0 |
x[5..7] | 否 | 是 | 1 |
y[5..7] | 否 | 是 | 0 |
如此可看出,通过人为控制,将x[0~3]与y[0~3]映射到 高速缓存不同的组时,可以将缓存的命中率提升到 75%,从而大大加快数据读取速度。
- 存储地址与高速缓存的映射
在一个计算机系统中,每个存储地址有m位,则可形成 M=2^m个地址。一个机器的高速缓存被组织为如下结构:
该缓存共有S=2^s个 高速缓存组。每个 组含有E个 高速缓存行。每行有一个B=2^b字节的数据块,一个有效位--标记该行是否有效(有无数据),还有t=m-(b+s)个标记位,唯一的标记高速缓存的特定行。
同时m位地址有如下结构:
这样,高速缓存的结构可以用元组(S, E, B, m)来描述。每一个存储地址,通过其中的组索引字段完成 组选择,标记字段 完成 行选择, 块偏移字段 完成 字偏移的确定,从而完成 存储地址到 高速缓存的映射。
- 为什么采用地位中间位做索引
如果采用高位索引,一些连续的存储器块就会映射到相同的高速缓存块中。如图“高位索引”前4位地址均映射到第一个高速缓存组,第二个4位映射到第二个高速缓存组中,依次类推。对于一个良好空间性的程序,进行数组的顺序扫描时,任意时刻高速缓存只保存着一个块大小的内容,效率低下。而采用中间位索引,相邻的块总是映射到不同的高速缓存行。
- 编写高速缓存友好的代码
基本准则:
1.将注意力放在内循环上,大部分计算和存储器访问都发生在这里。
2.按照数据对象在存储器中的存储顺序、以步长为1的来读取数据,从而使得程序的空间局部性最大。
3.当从存储器中读取一个数据对象时,就尽量使用它,从而使得程序的时间局部性最大。