为什么memcpy()的速度每4KB大幅下降?

时间:2014-01-10 08:03:15

标签: performance memory malloc memcpy cpu-cache

我测试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

Graphs of memcpy speed showing troughs every 4k

我想它必须与缓存相关,但我无法从以下缓存不友好的情况中找到原因:

由于这两种情况的性能下降是由不友好的循环引起的,这些循环将分散的字节读入缓存,浪费了缓存行的其余空间。

这是我的代码:

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中,并且下降幅度要小得多。

More graphs showing performance drops

PS

@Leeor提供了一种填充丢弃的方法,在pbuff_1pbuff_2之间添加了一个2KB的虚拟缓冲区。它有效,但我不确定Leeor的解释。

enter image description here

3 个答案:

答案 0 :(得分:30)

内存通常以4k页组织(尽管也支持更大的尺寸)。程序看到的虚拟地址空间可能是连续的,但在物理内存中并不一定如此。操作系统维护虚拟地址到物理地址的映射(在页面映射中)通常会尝试将物理页面保持在一起,但这并不总是可能的,并且它们可能会断开(特别是在长时间使用的情况下,它们可能会偶尔交换) )。

当您的内存流跨越4k页面边界时,CPU需要停止并获取新的转换 - 如果它已经看到该页面,则可以将其缓存在TLB中,并且访问被优化为最快的,但如果这是第一次访问(或者如果你有太多的页面让TLB保持),CPU将不得不停止内存访问并开始页面遍历页面映射条目 - 这相对较长,因为每个级别实际上是一个自己读取的内存(在虚拟机上它甚至更长,因为每个级别可能需要在主机上完整的页面行走)。

你的memcpy函数可能有另一个问题 - 当第一次分配内存时,操作系统只会将页面构建到页面映射,但由于内部优化而将它们标记为未访问和未修改。第一次访问不仅可以调用页面遍历,而且可能还有一个辅助,告诉OS该页面将被使用(并且存储到目标缓冲区页面中),这将花费昂贵的转换到某个OS处理程序。

为了消除这种噪音,请分配缓冲区一次,执行几次重复复制,并计算摊销时间。另一方面,这将为您提供“热情”的性能(即在缓存预热后),因此您将看到缓存大小反映在图表上。如果你想在不遭受分页延迟的情况下获得“冷”效果,你可能希望在迭代之间刷新缓存(只是确保你没有时间)

修改

重读问题,您似乎正在进行正确的测量。我的解释的问题是它应该在4k*i之后显示逐渐增加,因为在每次这样的下降你再次支付罚款,但是然后应该享受免费乘车直到下一个4k。它没有解释为什么会出现这种“尖峰”并且在它们之后速度恢复正常。

我认为 面临与您的问题中链接的关键步幅问题类似的问题 - 当您的缓冲区大小为4k时,两个缓冲区将对齐缓存中的相同集合,互相捶打。你的L1是32k,所以一开始似乎不是问题,但是假设数据L1有8种方式,它实际上是4k环绕到相同的集合,并且你有2 * 4k块具有完全相同的对齐(假设分配是连续进行的),因此它们在相同的集合上重叠。这足以让LRU无法完全按照您的预期工作,并且您将继续发生冲突。

为了检查这一点,我尝试在pbuff_1和pbuff_2之间使用malloc一个虚拟缓冲区,使其大2k并希望它打破对齐。

EDIT2:

好的,既然这样可行,那就是时候详细说明了。假设您在范围0x1000-0x1fff0x2000-0x2fff分配两个4k阵列。在L1中设置0将包含0x1000和0x2000的行,设置1将包含0x1040和0x2040,依此类推。在这些大小的情况下,你没有任何问题,它们可以共存而不会溢出缓存的关联性。但是,每次执行迭代时,您都有一个加载和一个存储访问同一个集合 - 我猜这可能会导致HW发生冲突。更糟糕的是 - 你需要多次迭代来复制一行,这意味着你拥有8个负载+ 8个存储(如果你向量化,但仍然很多),所有都针对相同的差集,我很漂亮确定那里隐藏着一堆碰撞。

我也看到Intel optimization guide有具体的说法(见3.6.8.2):

  

当代码访问两个不同的时,会发生4 KB内存别名   内存位置,它们之间有4 KB的偏移量。 4千字节   别名情况可以在内存复制例程中显示出来   源缓冲区和目标缓冲区的地址维护一个   常数偏移和常数偏移恰好是的倍数   从一次迭代到下一次迭代的字节增量。

     

...

     

负载必须等到商店退役之后才可以   继续。例如,在偏移16处,下一次迭代的负载是   4 KB的别名当前迭代存储,因此循环必须等待   直到商店操作完成,进行整个循环   序列化。等待所需的时间量随着更大而减少   偏移直到96的偏移解决了问题(因为没有未决的   加载时具有相同地址的商店)。

答案 1 :(得分:2)

我希望这是因为:

  • 当块大小为4KB倍数时,malloc会从操作系统中分配新页面。
  • 当块大小不是4KB倍数时,malloc会从其(已分配的)堆中分配一个范围。
  • 当从O / S分配页面时,它们是“冷”的:第一次触摸它们非常昂贵。

我的猜测是,如果你在第一个memcpy之前做一个gettimeofday,那么将“加热”分配的内存,你就不会看到这个问题了。而不是做一个初始的memcpy,即使在每个分配的4KB页面中写入一个字节也可能足以预热页面。

通常当我想要像你这样的性能测试时,我将其编码为:

// 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

答案 2 :(得分:0)

由于您多次循环,我认为关于未映射的页面的参数是无关紧要的。在我看来,你所看到的是硬件预取器不愿意越过页面边界以免引起(可能不必要的)页面错误的影响。