Why does the speed of memcpy() drop dramatically every 4KB?
我测试了EDOCX1的速度(0),注意到速度在i*4kb时急剧下降。结果表明:Y轴为速度(mb/s),X轴为
环境:
CPU:Intel(R)Xeon(R)CPU [email protected]
操作系统:2.6.35-22-通用33 Ubuntu
gcc编译器标志:-o3-msse4-dintel_sse4-wall-std=c99
我想它一定与缓存有关,但我无法从以下缓存不友好的情况中找到原因:
当循环遍历8192个元素时,为什么我的程序会变慢?
为什么512x512的矩阵转置要比513x513的矩阵转置慢得多?
由于这两种情况的性能下降是由不友好的循环导致的,这些循环将分散的字节读取到缓存中,从而浪费了缓存线的剩余空间。
这是我的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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 ", ((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秒内循环100000次。有趣的是,下降不仅发生在4Kb边界上,而且以4*i+2Kb的形式出现,下降幅度要小得多。
聚苯乙烯@Leeor提供了一种方法来填补这个空缺,在
内存通常组织在4K页中(尽管也支持更大的大小)。程序看到的虚拟地址空间可能是连续的,但在物理内存中不一定是连续的。维护虚拟到物理地址映射(在页面映射中)的操作系统通常也会尝试将物理页面保持在一起,但这并不总是可能的,它们可能会被断开(特别是在长时间使用时,它们可能会偶尔交换)。
当内存流跨越4K页边界时,CPU需要停止并获取新的转换-如果它已经看到了该页,则可能会将其缓存在TLB中,并且访问被优化为最快,但如果这是第一次访问(或者如果您有太多的页可供TLB保存),则CPU将不得不暂停内存访问A然后在页面映射条目上开始一次页面浏览——这相对较长,因为每个级别实际上都是一个自己读取的内存(在虚拟机上,甚至更长,因为每个级别可能需要在主机上进行一次完整的页面浏览)。
memcpy函数可能还有另一个问题——当第一次分配内存时,操作系统只会将页面构建到页面映射,但由于内部优化,将它们标记为未访问和未修改。第一次访问不仅可以调用页面浏览,还可以帮助告诉操作系统该页面将被使用(并存储到,用于目标缓冲区页面),这将花费昂贵的转换到某个操作系统处理程序。
为了消除这种噪声,分配一次缓冲区,对拷贝进行多次重复,并计算摊余时间。另一方面,这将给您"温暖"的性能(即,在缓存预热之后),这样您将看到缓存大小反映在图形上。如果您希望在不受分页延迟影响的情况下获得"冷"效果,那么您可能需要在迭代之间刷新缓存(只需确保不计时)
编辑重读这个问题,你似乎在做一个正确的测量。我解释的问题是,在
我认为您正面临一个与问题中链接的关键跨步问题类似的问题-当您的缓冲区大小是一个不错的4K圆形时,两个缓冲区将与缓存中的相同集对齐并相互攻击。你的l1是32K,所以一开始看起来不是问题,但是假设数据l1有8种方式,它实际上是4K环绕到相同的集合,并且你有2*4K块具有完全相同的对齐方式(假设分配是连续的),所以它们在相同的集合上重叠。足够了,LRU不能像你期望的那样工作,你会一直有冲突。
为了检查这一点,我会尝试在pbuff_1和pbuff_2之间插入一个虚拟缓冲区,使其变大2k,并希望它会破坏对齐。
编辑2:好吧,既然这个可行,是时候详细说明一下了。假设在
我还看到《英特尔优化指南》对此有具体的说明(请参阅3.6.8.2):
4-KByte memory aliasing occurs when the code accesses two different
memory locations with a 4-KByte offset between them. The 4-KByte
aliasing situation can manifest in a memory copy routine where the
addresses of the source buffer and destination buffer maintain a
constant offset and the constant offset happens to be a multiple of
the byte increment from one iteration to the next....
loads have to wait until stores have been retired before they can
continue. For example at offset 16, the load of the next iteration is
4-KByte aliased current iteration store, therefore the loop must wait
until the store operation completes, making the entire loop
serialized. The amount of time needed to wait decreases with larger
offset until offset of 96 resolves the issue (as there is no pending
stores by the time of the load with same address).
我想是因为:
- 当块大小是4KB的倍数时,那么
malloc 从O/S分配新页面。 - 当块大小不是4KB的倍数时,那么
malloc 从其(已分配的)堆中分配一个范围。 - 当从O/S分配页面时,它们是"冷的":第一次触摸它们是非常昂贵的。
我的猜测是,如果你在第一个
通常,当我需要像您这样的性能测试时,我会将其编码为:
1 2 3 4 5 6 7 8 9 10 | // Run in once to pre-warm the cache runTest(); // Repeat startTimer(); for (int i = count; i; --i) runTest(); stopTimer(); // use a larger count if the duration is less than a few seconds // repeat test 3 times to ensure that results are consistent |
由于你循环了很多次,我认为关于页面未被映射的争论是无关紧要的。在我看来,您看到的是硬件预取器不愿意跨越页面边界以避免(可能不必要的)页面错误的影响。