我测试了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
我想它必须与缓存相关,但我无法从以下缓存不友好的情况中找到原因:
为什么我的程序在完全循环8192个元素时会变慢?
为什么转换512x512的矩阵要比转换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); }UPDATE
考虑到来自@ 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
和之间添加了一个2KB的虚拟缓冲区pbuff_2
.它有效,但我不确定Leeor的解释.
内存通常以4k页组织(尽管也支持更大的内容).程序看到的虚拟地址空间可能是连续的,但在物理内存中并不一定如此.操作系统维护虚拟地址到物理地址的映射(在页面映射中)通常会尝试将物理页面保持在一起,但这并不总是可能的,并且它们可能会断开(特别是在长时间使用的情况下,它们可能会偶尔交换) ).
当您的内存流跨越4k页边界时,CPU需要停止并获取新的转换 - 如果它已经看到了页面,它可能会缓存在TLB中,并且访问被优化为最快,但如果这样是第一次访问(或者如果你有太多页面供TLB保留),CPU将不得不停止内存访问并在页面映射条目上开始页面遍历 - 这相对较长,因为每个级别实际上都是一个自己读取的内存(在虚拟机上它甚至更长,因为每个级别可能需要在主机上完整的页面行走).
你的memcpy函数可能有另一个问题 - 当第一次分配内存时,操作系统只会将页面构建到页面映射,但由于内部优化,将它们标记为未访问和未修改.第一次访问不仅可以调用页面遍历,而且可能还有一个帮助告诉操作系统将要使用该页面(并且存储到目标缓冲区页面中),这将花费昂贵的转换到某个OS处理程序.
为了消除这种噪声,请分配缓冲区一次,执行几次重复复制,并计算分摊的时间.另一方面,这将为您提供"热情"的性能(即在缓存预热后),因此您将看到缓存大小反映在您的图表上.如果你想在没有分页延迟的情况下获得"冷"效果,你可能想要在迭代之间刷新缓存(只是确保你没有时间)
重读问题,你似乎正在做一个正确的测量.我的解释的问题是它应该显示逐渐增加4k*i
,因为在每次这样的下降你再次支付罚款,但然后应享受免费乘车直到下一个4k.它没有解释为什么会出现这种"尖峰"并且在它们之后速度恢复正常.
我认为你正在面临着类似的问题在你的问题链接的关键步伐问题-当你的缓冲区的大小是一个很好的圆4K,两个缓冲区将调整到高速缓存中的同一组和捶打对方.你的L1是32k,所以一开始似乎不是问题,但是假设数据L1有8种方式,它实际上是4k环绕到相同的集合,并且你有2*4k块具有完全相同的对齐(假设分配是连续进行的),因此它们在相同的集合上重叠.LRU不能完全按照您的预期工作,并且您将继续存在冲突.
为了检查这一点,我尝试在pbuff_1和pbuff_2之间使用malloc一个虚拟缓冲区,使其大2k并希望它打破对齐.
好的,既然这样可行,那就是时候详细说明了.假设您在范围0x1000-0x1fff
和分配两个4k阵列0x2000-0x2fff
.在L1中设置0将包含0x1000和0x2000的行,设置1将包含0x1040和0x2040,依此类推.在这些大小的情况下,你没有任何问题,它们可以共存而不会溢出缓存的关联性.但是,每次执行迭代时,您都有一个加载和一个存储访问同一个集合 - 我猜这可能会导致HW发生冲突.更糟糕的是 - 你需要多次迭代来复制一行,这意味着你拥有8个负载+ 8个存储(如果你向量化,但仍然很多),所有都针对同一个糟糕的集合,我很漂亮确定那里隐藏着一堆碰撞.
我还看到英特尔优化指南有具体说明(见3.6.8.2):
当代码访问两个不同的内存位置并且它们之间有4 KB偏移时,会发生4 KB内存别名.4 KB的别名情况可以在存储器复制例程中体现,其中源缓冲器和目标缓冲器的地址保持恒定的偏移,并且常量偏移恰好是从一次迭代到下一次迭代的字节增量的倍数.
...
装载必须等到商店退役才能继续.例如,在偏移量16处,下一次迭代的加载是4 KB的别名当前迭代存储,因此循环必须等到存储操作完成,从而使整个循环序列化.等待所需的时间量随着偏移量的增加而减小,直到96的偏移量解决了问题(因为在具有相同地址的负载时没有待处理的存储).