为什么memcpy()的速度每4KB急剧下降?

我测试了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-generic#33-Ubuntu

GCC编译器标志:-O3 -msse4 -DINTEL_SSE4 -Wall -std = c99

memcpy速度图显示每4k低谷

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

  • 为什么我的程序在循环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.3fn", ((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_1pbuff_2之间添加了一个2KB的虚拟缓冲区。 它有效,但我不确定Leeor的解释。

    在这里输入图像描述


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

    当你的内存流超过4k页边界时,CPU需要停下来去获取一个新的翻译 - 如果它已经看到了页面,它可能被缓存在TLB中,并且访问被优化为最快,但如果这是第一次访问(或者如果你有太多的页面供TLB使用),CPU将不得不停止内存访问,并开始页面漫游页面映射条目 - 这是相当长的,因为每个级别都是事实一个内存自己读取(在虚拟机上它甚至更长,因为每个级别可能需要主机上的完整页面路径)。

    您的memcpy函数可能会遇到另一个问题 - 首次分配内存时,操作系统只会将页面构建到页面映射中,但由于内部优化将其标记为未访问且未修改。 第一次访问不仅可以调用页面遍历,而且可能还会通知操作系统该页面将被使用(并存储到目标缓冲区页面中),这将花费一些昂贵的过渡到某个OS处理程序。

    为了消除这种噪音,分配缓冲区一次,执行几次副本的重复,并计算摊销时间。 另一方面,这会给你“温暖”的表现(即在缓存变暖之后),所以你会看到缓存大小反映在你的图表上。 如果您希望在不受分页延迟影响的情况下获得“冷”效果,则可能需要在迭代之间刷新缓存(只要确保您没有时间)

    编辑

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

    我认为你面临的问题与你的问题中存在的关键步骤问题类似 - 当你的缓冲区大小是一个不错的四舍五入时,两个缓冲区将对齐到缓存中的相同集合并相互冲突。 你的L1是32k,所以它起初看起来不是什么问题,但假设数据L1有8种方式,实际上它是一个4k环绕到相同的集合,并且你有2 * 4k块,具有完全相同的对齐(假设分配是连续完成的),因此它们在相同的集合上重叠。 LRU不能像你期望的那样工作就足够了,你会一直存在冲突。

    为了检查这一点,我试图在pbuff_1和pbuff_2之间建立一个虚拟缓冲区,让它变成2k大,并且希望它能够打破这个对齐。

    EDIT2:

    好的,因为这个工作,现在是时候详细一点。 假设您分配两个4k阵列,范围为0x1000-0x1fff0x2000-0x2fff 。 在L1中设置0将包含0x1000和0x2000处的行,集合1将包含0x1040和0x2040,依此类推。 在这些尺寸下,你还没有任何颠簸问题,它们可以全部共存,而不会溢出缓存的关联性。 但是,每次执行迭代时,您都有一个负载和一个商店访问相同的集合 - 我猜这可能会导致硬件冲突。 更糟糕的是 - 你需要多次迭代才能复制一条线,这意味着你拥有8个负载+8个商店(如果你进行矢量化,但仍然很多),所有指向相同的穷人集合,我很漂亮肯定会有一堆隐藏在那里的碰撞。

    我还看到,英特尔优化指南对此有特别说明(参见3.6.8.2):

    当代码访问两个不同的内存位置并在它们之间有4千字节的偏移量时,会发生4 KB的内存别名。 4 KB锯齿情况可以在内存复制例程中体现,其中源缓冲区和目标缓冲区的地址保持恒定偏移量,并且恒定偏移量恰好是从一次迭代到下一次迭代的字节增量的倍数。

    ...

    装载必须等到商店已经退役,然后才能继续。 例如,在偏移量16处,下一次迭代的负载是4 KB别名的当前迭代存储,因此该循环必须等待,直到存储操作完成,从而使整个循环序列化。 等待所需的时间量随着偏移量的增加而减小,直到偏移量为96时解决问题(因为在加载时具有相同地址的时候没有待处理的存储器)。


    我期望这是因为:

  • 当块大小为4KB倍数时, malloc从O / S分配新的页面。
  • 当块大小不是4KB倍数时, malloc从其已分配的堆中分配一个范围。
  • 当从O / S分配页面时,它们是“冷的”:第一次触摸它们是非常昂贵的。
  • 我的猜测是,如果你在第一个gettimeofday之前做了一个单一的memcpy ,那么这会'分配'内存,并且你不会看到这个问题。 而不是做一个初始的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
    

    既然你循环了很多次,我认为关于未被映射的页面的争论是无关紧要的。 在我看来,你所看到的是硬件预取器不希望跨越页面边界的效果,以免造成(潜在不必要的)页面错误。

    链接地址: http://www.djcxy.com/p/15079.html

    上一篇: Why does the speed of memcpy() drop dramatically every 4KB?

    下一篇: Memory Allocation/Deallocation?