3.5 高速缓存优化
高速缓存可以对你的应用程序的性能产生戏剧性的影响。在深刻理解高速缓存的工作原理后,你可对代码结构进行安排,最大限度地发挥高速缓存性能。有关高速缓存结构的内容,参见2.2节。
3.5.1 线读入顺序(命令)
当对一个可高速缓冲的数据访问时,若该数据不在数据高速缓存中,将使整个高速缓存线从外部内存带入高速缓存,这就称之为线读入。对于奔腾或动态执行(P6-系列)处理器,这些数据按下列成组顺序以4个8字节段组成的成组读入。
第一个地址 第二个地址 第三个地址 第四个地址
0H 8H 10H 18H
8H 0H 18H 10H
10H 18H 0H 8H
18H 10H 8H 0H
数据可以按它们到达的顺序有效地使用。如果一个数据组按串行顺序读出,那么可按串行顺序进行访问。因此,每个数据项可在它从内存到达时即被使用。
3.5.2 在高速缓存线中的数据对齐
大小为32字节倍数的数组应在高速缓存线的起始位置。以32字节边界对齐,将充分发挥按序线读入的优势,并匹配高速缓存线大小。大小非32字节倍数的数组应以32或16字节边界开始(在高速缓存线的开始或中间),为了按32或16边界对齐,需要对数据进行填充。如果必要,尽量按填充后的空间来定位数据(变量或常量)。
3.5.3 与分配效果
动态执行(P6-系列)处理器有一个“按读分配写”的高速缓存,对应于奔腾处理器的“无写分配,通过写失败而写”的高速缓存。
在动态执行(P6-系列)处理器中,写操作发生但被写部分不在高速缓存时,整个32字节高速缓存线被读入。在奔腾处理器中,当被写部分不在高速缓存时,仅简单地写到内存中去。
由于连序存贮操作被合并为突发写,且将数据保存在高速缓存中可为后继的读取操作使用,使写分配通常是有利的,这就是动态执行(P6-系列)处理器采用这种写策略的原因,也是一些奔腾处理器的系统在设计L2 Cache时实现这种方式的原因。
在以下情况下,写分配有如下缺点:
仅对高速缓存线的一部分进行写操作。
整个高速缓存线未读入。
跨距超过32字节的高速缓存线。
对大地址(>8000)写。
当在一个应用程序中有大量的写操作,如下面例子所示,跨距大于32字节高速缓存线且数组为大数组时,对动态执行(P6-系列)处理器的每个写操作将使整个高速缓存线被读取。另外,这种读取将替换掉一条(有时两条)不用或很少用的高速缓存线。
这样将导致每次存贮时增加一次对高速缓存线的读取,并降低程序的执行速度。当一个程序中有大量的写操作时,将使性能降低。Erastothenes筛选程序是一个说明这种高速缓冲效果的简单例子。在这个例子中,一个大数组不断地按增大的步长将其特定的值赋0。
注意 这仅为一个表现高速缓存效果的简单例子。在代码中可使用很多其它的优化方法。
Erastothenes筛选例子:
boolean array [2..max]
for (i=2; i
array:=1;
}
for (i=2; i
if (array[i]) {
for(j=2; j
array[j]:=0; /* 这里我们对内存赋0产生了
j循环内对高速缓存线的读取*/
}
}
}
对这个特定的例子来说, 有两种有效的优化方法。第一种是通过改用位数组来减少数组大小,目的是降低 Cache线的读取次数。每二种是通过检查前一次写的值,降低对内存的读写次数(波动较大高速缓存线)。
3.5.3.1 优化方法1: 布尔
在上面的程序中,“Boolean”是一个字符型数组。在某些程序中,更好的方法是把“boolean”数组变成位数组,这样可以执行读——修改——写操作(因为高速缓存规程将每个读操作变成读——修改——写)。但在本例中,由于大多数的步长大于256比特(一个高速缓存线的位数)。故不能有效地提高性能。
3.5.3.2 优化方法2: 写前检查
另一种优化方法是在写前检查该值是否已经是0。
boolean array[2..max]
for(i=2; i
array:=1;
}
for(i=2; i
if(array[i]) {
for (j=2; j
if(array[j]!=0) { /* 检测该值是否已为0 */
array[j]:=0;
}
}
}
}
由于在大多数时候筛选程序的数据已经是0,所以可以把对外部总线的驱动次数降低一半。
通过预先检查,可以仅使用一个猝发总线周期读数据,并为每个不需要再写的缓存线节省一个猝发总线周期。由于对已修改的高速缓存线不再需要回写,可节省下额外的时钟周期。
注意 本操作仅对 P6-系列的处理器有意义,不能增加奔腾处理器的性能。因此,本操作不具备通用性。由于顺序存贮被合并为猝发写,高速缓存中的数据为下一次读取而保留,所以写分配在大多数系统中是一种通用的改善性能的方法,这也是为什么P6系列处理使用该策略,而一些基于奔腾处理器的系统在L2高速缓存中实现它的原因。