我读了这篇文章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代码来测试它。我在Xeon(R)E3-1230 V2上使用Ubuntu 64bit,ARMv6兼容处理器rev 7和Debian运行代码,并在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, ¶m);
//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是否有更高级的预取策略?
答案 0 :(得分:3)
简答(TL; DR):您正在访问未初始化的数据,您的第一个循环必须在定时循环内为整个阵列分配新的物理页面。
当我运行你的代码并依次评论每个部分时,我得到两个循环几乎相同的时间。但是,当我取消注释两个部分并一个接一个地运行它们时,我确实得到了相同的结果。这让我怀疑你也是这样做的,并且在比较第一个循环和第二个循环时遭受冷启动效果。它很容易检查 - 只需更换循环的顺序,看看第一个循环是否仍然较慢。
要避免,要么选择足够大的LENGTH
(取决于您的系统),这样您就不会从第一个循环中获得任何缓存优势来帮助第二个循环,或者只是添加整个数组的单个遍历&# 39;没有定时。
请注意,第二个选项并不能准确证明博客想要说的内容 - 内存延迟会掩盖执行延迟,因此您使用的缓存行数量并不重要,您&# 39;仍然受到内存访问时间(或更准确地说 - 带宽)的瓶颈
此外 - 使用-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)
现在请注意这里发生了什么 - 从理论上讲,只有当数组足够小才能进入某个缓存时,你才会期望预热是有意义的 - 在这种情况下{{1}你使用的对于大多数机器上的L3都太大了。但是,您忘记了页面地图 - 您并没有跳过加热数据本身 - 您首先避免初始化。这在现实生活中永远不会给你有意义的结果,但是由于这是一个你没有注意到的基准,你只是将垃圾数据乘以它的延迟。
这意味着您在第一个循环中访问的每个新页面都不会进入内存,它可能会出现页面错误,并且必须调用操作系统为其映射新的物理页面< / strong>即可。这是一个漫长的过程,乘以您使用的4K页数 - 累积到很长一段时间。在这个数组大小的情况下,您甚至无法从TLB中受益(您拥有16k不同的物理4k页,比大多数TLB甚至可以支持2级),因此它只是故障流的问题。这可能是任何分析工具的措施。
同一阵列上的第二次迭代不会产生这种影响并且速度会快得多 - 即使仍然需要在每个新页面上进行完整的页面行走(这纯粹是在HW中完成的),然后从内存中获取数据。
顺便说一下,这也是你对一些行为进行基准测试的原因,你多次重复同样的事情(在这种情况下,如果你用相同的步幅多次运行数组,它会解决你的问题,并且忽略前几轮。