我测试了 memcpy()
的速度,注意到速度在i * 4KB时急剧下降 . 结果如下:Y轴是速度(MB /秒),X轴是 memcpy()
的缓冲区大小,从1KB增加到2MB . 子图2和子图3详述了1KB-150KB和1KB-32KB的部分 .
环境:
CPU:Intel(R)Xeon(R)CPU E5620 @ 2.40GHz
操作系统:2.6.35-22-通用#33-Ubuntu
GCC编译器标志:-O3 -msse4 -DINTEL_SSE4 -Wall -std = c99
我想它必须与缓存相关,但我无法从以下缓存不友好的情况中找到原因:
-
Why is my program slow when looping over exactly 8192 elements?
-
Why is transposing a matrix of 512x512 much slower than transposing a matrix of 513x513?
由于这两种情况的性能下降是由不友好的循环引起的,这些循环将分散的字节读入高速缓存,浪费了高速缓存行的其余空间 .
这是我的代码:
void memcpy_speed(unsigned long buf_size, unsigned long iters){
struct timeval start, end;
unsigned char * pbuff_1;
unsigned char * pbuff_2;
pbuff_1 = malloc(buf_size);
pbuff_2 = malloc(buf_size);
gettimeofday(&start, NULL);
for(int i = 0; i < iters; ++i){
memcpy(pbuff_2, pbuff_1, buf_size);
}
gettimeofday(&end, NULL);
printf("%5.3f\n", ((buf_size*iters)/(1.024*1.024))/((end.tv_sec - \
start.tv_sec)*1000*1000+(end.tv_usec - start.tv_usec)));
free(pbuff_1);
free(pbuff_2);
}
更新
考虑到来自@ usr,@ ChrisW和@Leeor的建议,我更准确地重新测试了测试,下面的图表显示了结果 . 缓冲区大小从26KB到38KB,我每隔64B测试一次(26KB,26KB 64B,26KB 128B,......,38KB) . 每次测试在约0.15秒内循环100,000次 . 有趣的是,下降不仅恰好出现在4KB边界,而且还出现在4 * i 2 KB中,下降幅度要小得多 .
PS
@Leeor提供了一种填充丢弃的方法,在 pbuff_1
和 pbuff_2
之间添加了一个2KB的虚拟缓冲区 . 它有效,但我不确定Leeor的解释 .
3 回答
内存通常以4k页组织(尽管也支持更大的内容) . 程序看到的虚拟地址空间可能是连续的,但在物理内存中并不一定如此 . 操作系统维护虚拟地址到物理地址的映射(在页面映射中)通常会尝试将物理页面保持在一起,但这并不总是可能的,并且它们可能会断开(特别是在长时间使用的情况下,它们可能会偶尔交换) ) .
当您的内存流跨越4k页边界时,CPU需要停止并获取新的转换 - 如果它已经看到了页面,它可能会缓存在TLB中,并且访问被优化为最快,但如果这样是第一次访问(或者如果你有太多页面供TLB保留),CPU将不得不停止内存访问并在页面映射条目上开始页面遍历 - 这相对较长,因为每个级别实际上都是一个自己读取的内存(在虚拟机上它甚至更长,因为每个级别可能需要在主机上完整的页面行走) .
你的memcpy函数可能有另一个问题 - 当第一次分配内存时,操作系统只会将页面构建到页面映射,但由于内部优化,将它们标记为未访问和未修改 . 第一次访问不仅可以调用页面遍历,而且可能还有一个辅助,告诉OS该页面将被使用(并且存储到目标缓冲区页面中),这将花费昂贵的转换到某个OS处理程序 .
为了消除这种噪声,请分配缓冲区一次,执行几次重复复制,并计算分摊的时间 . 另一方面,这将为您提供“热情”的性能(即在缓存预热后),因此您将看到缓存大小反映在图表上 . 如果你想在没有分页延迟的情况下获得“冷”效果,你可能想要在迭代之间刷新缓存(只是确保你没有时间)
编辑
重读问题,你似乎正在做一个正确的测量 . 我的解释的问题是它应该在
4k*i
之后逐渐增加,因为在每次这样的下降你再次支付罚款,但是然后应该享受免费乘车直到下一个4k . 它没有解释为什么有这样的"spikes"并且在它们之后速度恢复正常 .我认为你面临着与你的问题中链接的关键步幅问题类似的问题 - 当你的缓冲区大小是一个很好的第4k轮时,两个缓冲区将对齐缓存中的相同集并相互颠簸 . 你的L1是32k,所以它实际上不是4k环绕到相同的集合,并且你有2 * 4k块具有完全相同的对齐(假设分配是连续完成的),所以它们在相同的集合上重叠 . 它完全按照你的预期工作,你会继续发生冲突 .
为了检查这一点,我尝试在pbuff_1和pbuff_2之间使用malloc一个虚拟缓冲区,使其大2k并希望它打破对准 .
编辑2:
好的,既然这样可行,那就是时候详细说明了 . 假设您在范围
0x1000-0x1fff
和0x2000-0x2fff
分配两个4k阵列 . 在L1中设置0将包含0x1000和0x2000的行,设置1将包含0x1040和0x2040,依此类推 . 在这些尺寸下,你不要猜测这可能会导致硬件冲突 . 更糟糕的是 - 你很清楚那里隐藏着一堆碰撞 .我也看到Intel optimization guide有具体的说法(见3.6.8.2):
当代码访问两个不同的内存位置并且它们之间有4 KB偏移时,会发生> 4 KB内存别名 . 4 KB的别名情况可以在存储器复制例程中显现,其中源缓冲器和目标缓冲器的地址保持恒定的偏移,并且常量偏移恰好是从一次迭代到下一次迭代的字节增量的倍数 . ...装载必须等到商店退役才能继续 . 例如,在偏移量16处,下一次迭代的加载是4 KB的别名当前迭代存储,因此循环必须等到存储操作完成,从而使整个循环序列化 . 等待所需的时间量随着偏移量的增加而减小,直到96的偏移量解决了问题(因为在具有相同地址的负载时没有待处理的存储) .
我希望这是因为:
当块大小为4KB倍数时,
malloc
从O / S分配新页面 .当块大小不是4KB倍数时,
malloc
将从其(已分配的)堆中分配一个范围 .当从O / S分配页面时,它们是'cold':第一次触摸它们非常昂贵 .
我的猜测是,如果你在第一个
gettimeofday
之前做了一个memcpy
那么那将是'warm'分配的内存,你不会看到这个问题 . 而不是做一个初始memcpy,甚至在每个分配的4KB页面中写入一个字节可能足以预热页面 .通常,当我想要像你一样的性能测试时,我将其编码为:
由于您循环多次,我认为关于未映射的页面的论点是无关紧要的 . 在我看来,你所看到的是硬件预取器不愿意越过页面边界以便不导致(可能不必要的)页面错误的影响 .