【问题标题】:memcpy significant performance differences when writing to buffer for multiple times多次写入缓冲区时,memcpy 显着的性能差异
【发布时间】:2022-01-15 11:35:15
【问题描述】:

当使用 memcpy 多次写入缓冲区时,我可以看到显着的性能差异:第一次写入特定地址比第二次或更多次写入需要更长的时间。 观察结果是 100% 可重复的。

我想知道什么会导致如此显着的性能差异?

参见以下代码示例(可在 Windows 上编译):

#include <iostream>
#include <profileapi.h>

LARGE_INTEGER getTimeStamp(void)
{
    LARGE_INTEGER t;
    QueryPerformanceCounter(&t);
    return t;
}

unsigned int getElapsedMicroseconds(LARGE_INTEGER start)
{
    LARGE_INTEGER end = getTimeStamp();
    
    LARGE_INTEGER freq;
    QueryPerformanceFrequency(&freq);

    double t = (double)(end.QuadPart - start.QuadPart) * 1000000.0 / (double)freq.QuadPart;
    return (unsigned int)(t + 0.5);
}

int main(int argc, char** argv)
{
    static const size_t singleBuffSize = 36 * 1024 * 1024;
    static const size_t nrOfBuffers = 6;

    unsigned char* srcBuff = new unsigned char[singleBuffSize];
    unsigned char* dstBuff = new unsigned char[singleBuffSize * nrOfBuffers];

    for (int i = 0; i < (nrOfBuffers*3); i++)
    {
        size_t buffIdx = (i % nrOfBuffers) * singleBuffSize;

        LARGE_INTEGER start = getTimeStamp();
        memcpy(&dstBuff[buffIdx], srcBuff, singleBuffSize);
        unsigned int elapsedMicroseconds = getElapsedMicroseconds(start);

        printf("Loop %2d: buffer nr %2lu, elapsed time = %6u microseconds\n", i+1, ((i % nrOfBuffers) + 1), elapsedMicroseconds);
    }
    
    delete[] srcBuff;
    delete[] dstBuff;
    
    return 0;
}

示例结果:

Loop  1: buffer nr  1, elapsed time =  76207 microseconds
Loop  2: buffer nr  2, elapsed time =  25552 microseconds
Loop  3: buffer nr  3, elapsed time =  24200 microseconds
Loop  4: buffer nr  4, elapsed time =  24036 microseconds
Loop  5: buffer nr  5, elapsed time =  28470 microseconds
Loop  6: buffer nr  6, elapsed time =  58528 microseconds
Loop  7: buffer nr  1, elapsed time =   6428 microseconds
Loop  8: buffer nr  2, elapsed time =   9324 microseconds
Loop  9: buffer nr  3, elapsed time =   9389 microseconds
Loop 10: buffer nr  4, elapsed time =   9434 microseconds
Loop 11: buffer nr  5, elapsed time =   9641 microseconds
Loop 12: buffer nr  6, elapsed time =   9953 microseconds
Loop 13: buffer nr  1, elapsed time =   9488 microseconds
Loop 14: buffer nr  2, elapsed time =   9834 microseconds
Loop 15: buffer nr  3, elapsed time =   6211 microseconds
Loop 16: buffer nr  4, elapsed time =   6282 microseconds
Loop 17: buffer nr  5, elapsed time =   5950 microseconds
Loop 18: buffer nr  6, elapsed time =   9570 microseconds

例如用于缓冲区 nr。 1、第一次memcpy调用比后续调用耗时长很多

【问题讨论】:

  • 阅读指令和数据缓存。如果这看起来不是一个因素,那么请仔细检查组件以寻找任何线索。
  • 编译时使用了哪些优化设置?我注意到没有标记volatile

标签: c++ memcpy


【解决方案1】:

源缓冲区的数据大小为 36MB,目标缓冲区的数据大小为 216MB。这远远超出了我买得起的任何计算机的缓存大小。这与缓存无关。

解释是为进程分配内存需要时间。第一个副本分配 72MB,接下来的五个副本每个分配 36MB,因此它们更快。从第七个副本开始,不再分配内存,所以从那时起我们每秒复制几个 GB。

这两个malloc只是保留空间,还没有分配内存。

【讨论】:

  • 有道理,谢谢。如果在上面的示例中,我在开始循环之前用零填充 216 MB 缓冲区,那么只有第一次访问需要更长的时间(因为还需要为 36 MB 数据源缓冲区分配内存)。如果我在开始循环之前另外用零填充数据源缓冲区,则循环中的所有 memcpy 调用将花费大约。同一时间。
  • 内存被分配的说法并不完全准确,因为new实际上分配了一些内存空间,除了它在虚拟内存中。虚拟页面不直接映射到物理内存。此映射通常在执行第一次触摸 时完成,这称为page fault。页面错误非常慢。使用更大的页面(例如大页面)或多线程来编写页面通常会有所帮助。
  • 虽然我看到你对乐观分配器有一点看法,但现在 cpus 有很大的缓存。 36M是标准的。 EPYC Zen3 有 256MB。我的桌面 Threadripper 有 128MB 的三级缓存。这都在同一个数量级内。
【解决方案2】:

更新:进行更多测试,我相信其他答案,这可能是由于乐观的内存分配多于缓存,或者可能是两者的结合。但是,它需要收集性能计数器才能得出结论。

从二级缓存读取最多需要 10 个周期,而在现代 CPU 中读取/写入 DDR(主内存)需要超过 300 个周期。它慢了 30 倍。

当您一次又一次地迭代内存时,碎片将越来越多地落入 L2/L3 缓存中,从而加快执行速度。

这是一个很好的阅读。 PDF 有点过时,但仍然 99% 有效。

What Every Programmer Should Know About Memory

【讨论】:

    猜你喜欢
    • 2013-11-04
    • 2020-09-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-27
    相关资源
    最近更新 更多