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

 dingzhi521 发布于 2023-02-04 17:33

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

每4k显示低谷的memcpy速度图

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

为什么我的程序在完全循环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的解释.

在此输入图像描述

1 个回答
  • 内存通常以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并希望它打破对齐.

    EDIT2:

    好的,既然这样可行,那就是时候详细说明了.假设您在范围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的偏移量解决了问题(因为在具有相同地址的负载时没有待处理的存储).

    2023-02-04 17:34 回答
撰写答案
今天,你开发时遇到什么问题呢?
立即提问
热门标签
PHP1.CN | 中国最专业的PHP中文社区 | PNG素材下载 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有