cpu cacheline和prefetch策略

我读了这篇文章http://igoro.com/archive/gallery-of-processor-cache-effects/ 。 文章说因为缓存行延迟,代码:

int[] arr = new int[64 * 1024 * 1024]; // Loop 1 for (int i = 0; i < arr.Length; i++) arr[i] *= 3; // Loop 2 for (int i = 0; i < arr.Length; i += 16) arr[i] *= 3; 

将几乎有相同的执行时间,我写了一些示例c代码来测试它。 我使用Ubuntu 64位运行Xeon(R)E3-1230 V2上的代码,使用Debian运行ARMv6兼容处理器rev 7,并在Core 2 T6600上运行它。 所有结果都不是文章所说的。

我的代码如下:

 long int jobTime(struct timespec start, struct timespec stop) { long int seconds = stop.tv_sec - start.tv_sec; long int nsec = stop.tv_nsec - start.tv_nsec; return seconds * 1000 * 1000 * 1000 + nsec; } int main() { struct timespec start; struct timespec stop; int i = 0; struct sched_param param; int * arr = malloc(LENGTH * 4); printf("---------sieofint %d\n", sizeof(int)); param.sched_priority = 0; sched_setscheduler(0, SCHED_FIFO, &param); //clock_gettime(CLOCK_MONOTONIC, &start); //for (i = 0; i < LENGTH; i++) arr[i] *= 5; //clock_gettime(CLOCK_MONOTONIC, &stop); //printf("step %d : time %ld\n", 1, jobTime(start, stop)); clock_gettime(CLOCK_MONOTONIC, &start); for (i = 0; i < LENGTH; i += 2) arr[i] *= 5; clock_gettime(CLOCK_MONOTONIC, &stop); printf("step %d : time %ld\n", 2, jobTime(start, stop)); } 

每次我选择一个编译和运行(注释一个并取消注释另一个)。 编译:

 gcc -O0 -o cache cache.c -lrt 

在Xeon上我得到了这个:

 step 1 : 258791478 step 2 : 97875746 

我想知道这篇文章说的是否正确? 或者,最新的cpu有更高级的预取策略吗?

简答(TL; DR):您正在访问未初始化的数据,您的第一个循环必须在定时循环内为整个arrays分配新的物理页面。


当我运行你的代码并依次评论每个部分时,我得到两个循环几乎相同的时间。 但是,当我取消注释两个部分并一个接一个地运行它们时,我确实得到了相同的结果。 这让我怀疑你也是这样做的,并且在比较第一个循环和第二个循环时遭受冷启动效应。 它很容易检查 – 只需替换循环的顺序,看看第一个循环是否仍然较慢。

要避免,要么选择一个足够大的LENGTH (取决于你的系统),这样你就不会从第一个循环中获得任何缓存优势来帮助第二个循环,或者只是添加一个没有定时的整个数组的遍历。

请注意,第二个选项并不能完全certificate博客想说的内容 – 内存延迟会掩盖执行延迟,因此无论您使用多少个缓存行元素,您仍然会受到内存访问的瓶颈时间(或更准确地说 – 带宽)

此外 – 使用-O0对代码进行基准测试是一种非常糟糕的做法


编辑:

这是我得到的(删除了调度,因为它不相关)。
这段代码:

 for (i = 0; i < LENGTH; i++) arr[i] = 1; // warmup! clock_gettime(CLOCK_MONOTONIC, &start); for (i = 0; i < LENGTH; i++) arr[i] *= 5; clock_gettime(CLOCK_MONOTONIC, &stop); printf("step %d : time %ld\n", 1, jobTime(start, stop)); clock_gettime(CLOCK_MONOTONIC, &start); for (i = 0; i < LENGTH; i+=16) arr[i] *= 5; clock_gettime(CLOCK_MONOTONIC, &stop); 

给:

 ---------sieofint 4 step 1 : time 58862552 step 16 : time 50215446 

评论预热线时,与第二个循环中报告的优势相同:

 ---------sieofint 4 step 1 : time 279772411 step 16 : time 50615420 

替换循环的顺序(仍然注释了预热)表明它确实与步长无关,而是与排序有关:

 ---------sieofint 4 step 16 : time 250033980 step 1 : time 59168310 

(gcc版本4.6.3,关于Opteron 6272)

现在请注意这里发生了什么 - 从理论上讲,只有当arrays足够小才能放在某个缓存中时,你才会期望预热是有意义的 - 在这种情况下,你使用的LENGTH对于大多数机器上的L3来说太大了。 但是,您忘记了页面映射 - 您不仅仅是跳过了数据本身 - 您首先避免初始化它 。 这在现实生活中永远无法给你带来有意义的结果,但是由于这是你没注意到的基准,你只是将垃圾数据乘以它的延迟。

这意味着您在第一个循环中访问的每个新页面不仅会进入内存, 它可能会出现页面错误,并且必须调用操作系统为其映射新的物理页面 。 这是一个漫长的过程,乘以您使用的4K页数 - 累积到很长一段时间。 在这个数组大小你甚至不能受益于TLB(你有16k不同的物理4k页,比大多数TLB甚至可以支持2级),所以这只是故障流的问题。 这可能是任何分析工具的测量。

同一个数组上的第二次迭代不会产生这种影响并且速度会快得多 - 即使仍然需要在每个新页面上完成一个完整的页面行走(这完全是在HW中完成),然后从内存中获取数据。

顺便说一句,这也是你对一些行为进行基准测试的原因,你多次重复同样的事情(在这种情况下,如果你用相同的步幅多次运行数组,它会解决你的问题,并忽略第一个几轮)。