提交 5b02e9be 编写于 作者: W wizardforcel

ch7.

上级 e38db311
......@@ -20,7 +20,7 @@
+ 指令寄存器(IR),它含有当前执行的指令的机器码。
+ 栈指针(SP),它含有当前函数栈帧的指针,其中包含函数参数和局部变量。
+ 程序当前使用的存放数据的通用寄存器。
+ 状态寄存器,或者位集训器,含有当前计算的信息。例如,位寄存器通常含有移位来存储上个操作是否是零的结果。
+ 状态寄存器,或者位寄存器,含有当前计算的信息。例如,位寄存器通常含有移位来存储上个操作是否是零的结果。
在程序运行之中,CPU执行下列步骤,叫做“指令周期”:
......@@ -65,3 +65,107 @@ Th + m * Tp
当缺失率很低时平均访问时间趋近于`Th`,也就是说,程序可以表现为内存具有缓存的速度那样。
## 7.3 局部性
当程序首次读取某个字节时,缓存通常加载一“块”或一“行”数据,包含所需的字节和一些相邻数据。如果程序继续读取这些相邻数据,它们就已经在缓存中了。
例如,假设块大小是64B,你读取一个长度为64的字符串,字符串的首个字节恰好在块的开头。当你加载首个字节之后,你触发了缺失惩罚,但是之后字符串的剩余部分都在缓存中。在读取整个字符串之后,命中率是63/64。如果字符串被分在两个块中,你应该会触发两次缺失惩罚。但是这个命中率是62/64,约为97%。
另一方面,如果程序不可预测地跳来跳去,从内存中零散的位置读取数据,很少两次访问到相同的位置,缓存的性能就会很低。
程序使用相同数据多于一次的倾向叫做“时间局部性”。使用相邻位置的数据的倾向叫做“空间局部性”。辛运的是,许多程序天生就带有这两种局部性:
+ 许多程序含有非跳转或分支的代码块。由于这些代码块,指令顺序指令,访问模式具有空间局部性。
+ 在循环中,程序执行多次相同指令,所以访问模式具有时间局部性。
+ 一条指令的结果通常用于下一指令的操作数,所以数据访问模式具有时间局部性。
+ 当程序执行某个函数时,它的参数和局部变量在栈上储存在一起。这些值的访问具有空间局部性。
+ 最普遍的处理模型之一就是顺序读写数据元素。这一模式也具有空间局部性。
下一节中我们会探索程序的访问模式和缓存性能的关系。
## 7.4 缓存性能的度量
当我还是UC伯克利的毕业生时,我是Brian Harvey计算机体系结构课上的助教。我最喜欢的练习之一涉及到一个迭代数组,读写元素并度量平均时间的程序。通过改变数组的大小,就有可能推测出缓存的大小,块的大小,和一些其它属性。
我的这一程序的修改版本在本书仓库的`cache`目录下。
程序的核心部分是个循环:
```c
iters = 0;
do {
sec0 = get_seconds();
for (index = 0; index < limit; index += stride)
array[index] = array[index] + 1;
iters = iters + 1;
sec = sec + (get_seconds() - sec0);
} while (sec < 0.1);
```
内部的`for`循环遍历了数组。`limit`决定数组遍历的范围。`stride`决定跳过多少元素。例如,如果`limit`是16,`stride`是4,循环就会访问0、4、8、和12。
`sec`跟踪了CPU用于内循环的的全部时间。外部循环直到`sec`超过0.1秒才会停止,这对于我们计算出平均时间所需的精确度已经足够长了。
`get_seconds`使用系统调用`clock_gettime`,将结果换算成秒,并且以`double`返回结果。
```c
double get_seconds(){
struct timespec ts;
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts);
return ts.tv_sec + ts.tv_nsec / 1e9;
}
```
![](http://greenteapress.com/thinkos/html/thinkos001.png)
图 7.1:数据大小和间隔的平均缺失惩罚函数
为了将访问数据的时间分离出来,程序运行了第二个循环,它除了内循环不访问数据之外完全相同。它总是增加相同的变量:
```c
iters2 = 0;
do {
sec0 = get_seconds();
for (index = 0; index < limit; index += stride)
temp = temp + index;
iters2 = iters2 + 1;
sec = sec - (get_seconds() - sec0);
} while (iters2 < iters);
```
第二个循环运行和第一个循环相同数量的迭代。在每轮迭代之后,它从`sec`中减少了消耗的时间。当循环完成时,`sec`包含了所有数组访问的总时间,减去用于增加`temp`的时间。其中的差就是所有访问触发的全部缺失惩罚。最后,我们将它除以访问总数来获取每次访问的平均缺失惩罚,以ns为单位:
```
sec * 1e9 / iters / limit * stride
```
如果你编译并运行`cache.c`,你应该看到这样的输出:
```
Size: 4096 Stride: 8 read+write: 0.8633 ns
Size: 4096 Stride: 16 read+write: 0.7023 ns
Size: 4096 Stride: 32 read+write: 0.7105 ns
Size: 4096 Stride: 64 read+write: 0.7058 ns
```
如果你安装了Python和Matplotlib,你可以使用`graph_data.py`来使结果变成图形。图7.1展示了我运行在Dell Optiplex 7010上的结果。要注意数组大小和间隔以字节为单位表述,并不是数组元素数量。
花一分钟来考虑这张图片,并且看看你是否能推断出缓存信息。下面是一些需要思考的事情:
+ 程序多次遍历并读取数组,所以有大量的时间局部性。如果整个数组能放进缓存,平均缺失惩罚应几乎为0。
+ 当间隔是4的时候,我们读取了数组的每个元素,所以程序有大量的空间局部性。如果块大小足以包含64个元素,例如,即使数组不能完全放在缓存中,命中率应为63/64。
+ 如果间隔等于块的大小(或更大),空间局部性应为0,因为每次我们读取一个块的时候,我们只访问一个元素。这种情况下,我们会看到最大的缺失惩罚。
总之,如果数组比缓存大小更小,或间隔小于块的大小,我们认为会有良好的缓存性能。如果数组大于缓存大小,并且间隔较大时,性能只会下降。
在图7.1中,缓存性能对于所有间隔很好,只要数组小于`2 ** 22`字节。我们可以推测缓存大小近似4MiB。实际上,根据规范应该是3MiB。
当间隔为8、16或32B时,缓存性能良好。在64B时开始下降,对于更大的间隔,平均缺失惩罚约为9ns。我们可以推断出块大小为128B。
许多处理器都使用了“多级缓存”,它包含一个小型快速的缓存,和一个大型慢速的缓存。这个例子中,当数组大小大于`2 ** 14`B时,缺失惩罚似乎增长了一点。所以这个处理器可能也拥有一个访问时间小于1ns的16KB缓存。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册