首页 文章

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

提问于
浏览
50

我测试了 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 回答

  • 30

    内存通常以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-0x1fff0x2000-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的偏移量解决了问题(因为在具有相同地址的负载时没有待处理的存储) .

  • 0

    我希望这是因为:

    • 当块大小为4KB倍数时, malloc 从O / S分配新页面 .

    • 当块大小不是4KB倍数时, malloc 将从其(已分配的)堆中分配一个范围 .

    • 当从O / S分配页面时,它们是'cold':第一次触摸它们非常昂贵 .

    我的猜测是,如果你在第一个 gettimeofday 之前做了一个 memcpy 那么那将是'warm'分配的内存,你不会看到这个问题 . 而不是做一个初始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

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

相关问题