如何提高 memcpy 的性能

Posted

技术标签:

【中文标题】如何提高 memcpy 的性能【英文标题】:How to increase performance of memcpy 【发布时间】:2011-05-14 17:17:58 【问题描述】:

总结:

memcpy 在我的系统上的真实或测试应用程序中似乎无法以超过 2GB/秒的速度传输。如何获得更快的内存到内存副本?

详细信息:

作为数据捕获应用程序的一部分(使用一些专用硬件),我需要将大约 3 GB/秒的速度从临时缓冲区复制到主内存中。为了获取数据,我为硬件驱动程序提供了一系列缓冲区(每个 2MB)。硬件 DMA 将数据发送到每个缓冲区,然后在每个缓冲区已满时通知我的程序。我的程序清空缓冲区(memcpy 到另一个更大的 RAM 块),并将处理后的缓冲区重新发布到卡上以再次填充。我在使用 memcpy 足够快地移动数据时遇到问题。似乎内存到内存的复制速度应该足够快,可以在我正在运行的硬件上支持 3GB/秒。 Lavalys EVEREST 为我提供了 9337MB/秒的内存复制基准测试结果,但我无法使用 memcpy 达到这样的速度,即使在一个简单的测试程序中也是如此。

我已通过在缓冲区处理代码中添加/删除 memcpy 调用来隔离性能问题。如果没有 memcpy,我可以运行完整的数据速率——大约 3GB/秒。启用 memcpy 后,我的速度被限制在大约 550Mb/秒(使用当前编译器)。

为了在我的系统上对 memcpy 进行基准测试,我编写了一个单独的测试程序,它只对某些数据块调用 memcpy。 (我在下面发布了代码)我已经在我正在使用的编译器/IDE(National Instruments CVI)以及 Visual Studio 2010 中运行了它。虽然我目前没有使用 Visual Studio,但我愿意如果它会产生必要的性能,则进行切换。但是,在盲目地移动之前,我想确保它能够解决我的 memcpy 性能问题。

Visual C++ 2010:1900 MB/秒

NI CVI 2009:550 MB/秒

虽然 CVI 明显比 Visual Studio 慢我并不感到惊讶,但我对 memcpy 性能如此之低感到惊讶。虽然我不确定这是否可以直接比较,但这远低于 EVEREST 基准带宽。虽然我不需要那么高的性能,但至少需要 3GB/秒。标准库的实现肯定不会比 EVEREST 使用的差这么多!

如果有的话,在这种情况下我可以做些什么来让 memcpy 更快?


硬件细节: AMD Magny Cours - 4x 八核 128GB DDR3 Windows Server 2003 企业版 X64

测试程序:

#include <windows.h>
#include <stdio.h>

const size_t NUM_ELEMENTS = 2*1024 * 1024;
const size_t ITERATIONS = 10000;

int main (int argc, char *argv[])

    LARGE_INTEGER start, stop, frequency;

    QueryPerformanceFrequency(&frequency);

    unsigned short * src = (unsigned short *) malloc(sizeof(unsigned short) * NUM_ELEMENTS);
    unsigned short * dest = (unsigned short *) malloc(sizeof(unsigned short) * NUM_ELEMENTS);

    for(int ctr = 0; ctr < NUM_ELEMENTS; ctr++)
    
        src[ctr] = rand();
    

    QueryPerformanceCounter(&start);

    for(int iter = 0; iter < ITERATIONS; iter++)
        memcpy(dest, src, NUM_ELEMENTS * sizeof(unsigned short));

    QueryPerformanceCounter(&stop);

    __int64 duration = stop.QuadPart - start.QuadPart;

    double duration_d = (double)duration / (double) frequency.QuadPart;

    double bytes_sec = (ITERATIONS * (NUM_ELEMENTS/1024/1024) * sizeof(unsigned short)) / duration_d;

    printf("Duration: %.5lfs for %d iterations, %.3lfMB/sec\n", duration_d, ITERATIONS, bytes_sec);

    free(src);
    free(dest);

    getchar();

    return 0;

编辑:如果您有额外的 5 分钟时间并想做出贡献,您可以在您的机器上运行上述代码并将您的时间作为评论发布吗?

【问题讨论】:

我的笔记本显示相同的内存带宽。但是一个快速设计的 sse2/4 算法并没有提高性能(只有一点点)。 使用 SSE 代码进行更多测试只会使 VC2010 中的 memcpy 算法的速度提高 60 MB/秒。 Core-i5 笔记本电脑的峰值约为 2,224 GB/秒(这个数字不应该翻倍吗?我们正在写入这个数字并同时读取它,所以 ~4,4 GB/秒......)。要么可以做一些我忽略的事情,要么你真的必须“不复制”你的数据。 查看 onemasse 的答案(William Chan 的 memcpy 的 SSE2 ASM 实现) - 使用 memcpy 和 CopyMemory,我得到 1.8GB/s。通过 William 的实现,我得到了 3.54GB/s(几乎翻了一番!)。这是在 Core2Duo wolfdale 上,带有 2 通道 DDR2,频率为 800MHz。 在我下面的回答中,我刚刚想到从采集卡传输数据会消耗一些 CPU 可用的内存带宽,我想你会损失大约 33% (memcpy = 读/写,带采集卡 = 写/读/写),因此您的应用内 memcpy 将比基准 memcpy 慢。 Macbook Retina Pro Core,i7 2.6GHz(Win 7 x64 via Bootcamp):8474 MB/秒。编译器是 Embarcadero C++Builder 2010 【参考方案1】:

我找到了一种在这种情况下提高速度的方法。我写了一个多线程版本的 memcpy,在线程之间分割要复制的区域。以下是一组块大小的一些性能缩放数字,使用与上面相同的时序代码。我不知道性能,特别是对于这么小的块,会扩展到这么多线程。我怀疑这与这台机器上的大量内存控制器(16 个)有关。

Performance (10000x 4MB block memcpy):

 1 thread :  1826 MB/sec
 2 threads:  3118 MB/sec
 3 threads:  4121 MB/sec
 4 threads: 10020 MB/sec
 5 threads: 12848 MB/sec
 6 threads: 14340 MB/sec
 8 threads: 17892 MB/sec
10 threads: 21781 MB/sec
12 threads: 25721 MB/sec
14 threads: 25318 MB/sec
16 threads: 19965 MB/sec
24 threads: 13158 MB/sec
32 threads: 12497 MB/sec

我不明白 3 到 4 个线程之间的巨大性能跳跃。什么会导致这样的跳跃?

我已经包含了我在下面为可能遇到相同问题的其他代码编写的 memcpy 代码。请注意,此代码中没有错误检查 - 这可能需要为您的应用程序添加。

#define NUM_CPY_THREADS 4

HANDLE hCopyThreads[NUM_CPY_THREADS] = 0;
HANDLE hCopyStartSemaphores[NUM_CPY_THREADS] = 0;
HANDLE hCopyStopSemaphores[NUM_CPY_THREADS] = 0;
typedef struct

    int ct;
    void * src, * dest;
    size_t size;
 mt_cpy_t;

mt_cpy_t mtParamters[NUM_CPY_THREADS] = 0;

DWORD WINAPI thread_copy_proc(LPVOID param)

    mt_cpy_t * p = (mt_cpy_t * ) param;

    while(1)
    
        WaitForSingleObject(hCopyStartSemaphores[p->ct], INFINITE);
        memcpy(p->dest, p->src, p->size);
        ReleaseSemaphore(hCopyStopSemaphores[p->ct], 1, NULL);
    

    return 0;


int startCopyThreads()

    for(int ctr = 0; ctr < NUM_CPY_THREADS; ctr++)
    
        hCopyStartSemaphores[ctr] = CreateSemaphore(NULL, 0, 1, NULL);
        hCopyStopSemaphores[ctr] = CreateSemaphore(NULL, 0, 1, NULL);
        mtParamters[ctr].ct = ctr;
        hCopyThreads[ctr] = CreateThread(0, 0, thread_copy_proc, &mtParamters[ctr], 0, NULL); 
    

    return 0;


void * mt_memcpy(void * dest, void * src, size_t bytes)

    //set up parameters
    for(int ctr = 0; ctr < NUM_CPY_THREADS; ctr++)
    
        mtParamters[ctr].dest = (char *) dest + ctr * bytes / NUM_CPY_THREADS;
        mtParamters[ctr].src = (char *) src + ctr * bytes / NUM_CPY_THREADS;
        mtParamters[ctr].size = (ctr + 1) * bytes / NUM_CPY_THREADS - ctr * bytes / NUM_CPY_THREADS;
    

    //release semaphores to start computation
    for(int ctr = 0; ctr < NUM_CPY_THREADS; ctr++)
        ReleaseSemaphore(hCopyStartSemaphores[ctr], 1, NULL);

    //wait for all threads to finish
    WaitForMultipleObjects(NUM_CPY_THREADS, hCopyStopSemaphores, TRUE, INFINITE);

    return dest;


int stopCopyThreads()

    for(int ctr = 0; ctr < NUM_CPY_THREADS; ctr++)
    
        TerminateThread(hCopyThreads[ctr], 0);
        CloseHandle(hCopyStartSemaphores[ctr]);
        CloseHandle(hCopyStopSemaphores[ctr]);
    
    return 0;

【讨论】:

相当老的线程,但我想我会添加一些东西:缓存行一致性。查一下。可能解释了巨大的跳跃。当然,只是偶然。了解了这一点(Sutter 对此进行了描述),您可以创建一个智能 memcpy,利用它进行近乎完美的缩放。 @Robinson:绝对值得一看。在过去的几年里,我想我已经得出结论,这最终是一个 NUMA 性能问题。 FWIW,我在 i5-2430M 笔记本电脑上试过你的代码。线程数几乎没有区别。 1、2、4、8线程速度基本一样。我发现最快的 memcpy 来自 hapalibashi 对这个问题的回答:***.com/questions/1715224/…. @leecbaker,4+ 线程性能的巨大飞跃来自缓存。当 1、2 或 3 个内核正在运行您的副本时,还有另一个 CPU 正在运行其他东西或空闲。缓存几乎从不动态分布,因此整个 CPU 缓存不用于缓存您的读取和存储,当您生成 4 个以上线程时就是这种情况。另外,您的代码肯定是错误的,只需查看计算每个线程的副本大小的代码即可。【参考方案2】:

我不确定它是在运行时完成还是必须在编译时完成,但您应该启用 SSE 或类似的扩展,因为矢量单元通常可以将 128 位写入内存,而 64 位CPU。

试试this implementation。

是的,并确保源和目标都与 128 位对齐。如果你的源和目标没有相互对齐,你的 memcpy() 将不得不做一些严肃的魔法。 :)

【讨论】:

您需要将 /both/ source 和 dest 对齐到 16 字节(不是 32 位)。陈伟霆的代码使用的是 movdqa(a 表示对齐)。见siyobik.info/index.php?module=x86&id=183。您还应该为最后的性能下降分配缓存对齐的内存。 是的,我说“至少”。但是,如果您想做基于向量的 I/O,那么将数据对齐到 128 位当然是有意义的。我已经更正了我的答案。 啊。我以为您的意思是您在链接中发布的实现。【参考方案3】:

需要注意的一点是,您的进程(以及 memcpy() 的性能)会受到操作系统任务调度的影响 - 很难说这在您的时间安排中占多大的比例,但实际上是难以控制。设备 DMA 操作不受此限制,因为一旦启动它就不会在 CPU 上运行。由于您的应用程序是一个实际的实时应用程序,因此您可能需要尝试使用 Windows 的进程/线程优先级设置(如果您还没有的话)。请记住,您必须小心这一点,因为它会对其他流程(以及机器上的用户体验)产生非常负面的影响。

要记住的另一件事是操作系统内存虚拟化可能会在这里产生影响 - 如果您要复制到的内存页面实际上没有物理 RAM 页面支持,memcpy() 操作将导致操作系统出错获得物理支持。您的 DMA 页面可能被锁定在物理内存中(因为它们必须用于 DMA 操作),因此memcpy() 的源内存在这方面可能不是问题。您可能会考虑使用 Win32 VirtualAlloc() API 来确保您的 memcpy() 的目标内存已提交(我认为 VirtualAlloc() 是正确的 API,但我可能忘记了一个更好的 API - 它是自从我需要做这样的事情以来已经有一段时间了)。

最后,看看你是否可以使用the technique explained by Skizz 来完全避免memcpy() - 如果资源允许,这是你最好的选择。

【讨论】:

锁定页面是 SetProcessWorkingSetSize 和 VirtualLock。【参考方案4】:

在获得所需的内存性能方面存在一些障碍:

    带宽 - 数据从内存移动到 CPU 再返回的速度存在限制。根据this Wikipedia article 的说法,266MHz DDR3 RAM 的上限约为 17GB/s。现在,使用 memcpy,您需要将其减半以获得最大传输速率,因为数据被读取然后写入。从您的基准测试结果来看,您似乎没有在系统中运行尽可能快的 RAM。如果您负担得起,请升级主板/内存(而且价格不便宜,英国的超频者目前以 400 英镑的价格购买 3x4GB PC16000)

    操作系统 - Windows 是一种抢占式多任务操作系统,因此您的进程会经常暂停,以允许其他进程查看并执行操作。这将破坏您的缓存并停止传输。在最坏的情况下,您的整个进程可能会被缓存到磁盘!

    CPU - 被移动的数据还有很长的路要走:RAM -> L2 Cache -> L1 Cache -> CPU -> L1 -> L2 -> RAM。甚至可能有一个 L3 缓存。如果你想涉及 CPU,你真的想在复制 L1 的同时加载 L2。不幸的是,现代 CPU 运行 L1 缓存块的速度比加载 L1 所需的时间要快。 CPU 有一个内存控制器,在您将流数据按顺序输入 CPU 但仍然会遇到问题的情况下,它可以提供很大帮助。

当然,做某事的更快方法是不做。捕获的数据是否可以写入 RAM 中的任何位置,或者是在固定位置使用的缓冲区。如果您可以在任何地方编写它,那么您根本不需要 memcpy。如果它是固定的,您可以就地处理数据并使用双缓冲类型系统吗?也就是说,开始捕获数据,当它半满时,开始处理数据的前半部分。当缓冲区已满时,开始将捕获的数据写入开始并处理后半部分。这要求算法能够比采集卡产生的数据更快地处理数据。它还假设数据在处理后被丢弃。实际上,这是一个将转换作为复制过程的一部分的 memcpy,因此您有:

load -> transform -> save
\--/                 \--/
 capture card        RAM
   buffer

代替:

load -> save -> load -> transform -> save
\-----------/
memcpy from
capture card
buffer to RAM

或者获得更快的 RAM!

编辑:另一种选择是处理数据源和 PC 之间的数据——你能在里面放一个 DSP/FPGA 吗?定制硬件总是比通用 CPU 快。

另一个想法:我已经有一段时间没有做任何高性能图形的东西了,但是你能把数据 DMA 到图形卡然后再 DMA 出来吗?您甚至可以利用 CUDA 进行一些处理。这将使 CPU 完全脱离内存传输循环。

【讨论】:

Skizz,我没有对数据进行任何数学处理,因为它只是复制到不同的缓冲区,因此使用另一个 DMA 或 DSP/FPGA 将无济于事。数据确实是通过双缓冲区系统进入的——实际上是一个包含 4 个或更多缓冲区的队列,并被复制到一个静态长缓冲区 (10GB+)。 关于更快的RAM:系统目前有16个通道的PC3-10600,额定10.7GB/s的理论峰值传输速率(每个通道)。虽然我意识到我什至无法接近这个峰值等级,但我认为我应该在 RAM 的硬件性能方面仍有一些空间。 @leecbaker:那么数据发生了什么变化? 数据采集并存储在RAM中,所有数据采集完毕后,整批处理。集合是我关心的性能敏感部分。【参考方案5】:

首先,您需要检查内存是否在 16 字节边界上对齐,否则会受到处罚。这是最重要的。

如果您不需要符合标准的解决方案,您可以通过使用某些编译器特定的扩展名(例如 memcpy64)来检查情况是否有所改善(如果有可用的内容,请查看您的编译器文档)。事实上memcpy必须能够处理单字节复制,但如果你没有这个限制,一次移动 4 或 8 个字节会快得多。

再次,您可以选择编写内联汇编代码吗?

【讨论】:

内联汇编是一种选择,但这里的其他评论者指出,它并没有产生显着的改进。另外,我刚刚验证了所有内存块都是 16 字节对齐的。 你能在这里发布什么程序集生成你的编译器吗?【参考方案6】:

也许您可以进一步解释一下您是如何处理更大的内存区域的?

是否可以在您的应用程序中简单地传递缓冲区的所有权,而不是复制它?这将完全消除问题。

或者您使用memcpy 不仅仅是为了复制?也许您正在使用更大的内存区域从您捕获的内容中构建顺序数据流?特别是如果您一次处理一个字符,您可能会遇到一半。例如,可以调整处理代码以适应表示为“缓冲区数组”而不是“连续内存区域”的流。

【讨论】:

在数据捕获期间,我没有对存储缓冲区中的数据做任何事情。它会在以后转储到文件中。 是否可以直接捕获到更大的内存区域?您可以按顺序构建一个缓冲区指针数组,然后将它们写出来。 (您也许甚至可以使用WriteFileGather 来获得向量IO,但它有一些相当严格的对齐要求。)【参考方案7】:

您可以使用 SSE2 寄存器编写更好的 memcpy 实现。 VC2010 中的版本已经这样做了。所以问题更多,如果你正在处理它对齐的内存。

也许你可以比 VC 2010 版本做得更好,但它确实需要一些了解,如何做到这一点。

PS:您可以通过反向调用将缓冲区传递给用户模式程序,以完全防止复制。

【讨论】:

【参考方案8】:

我建议您阅读的一个来源是 MPlayer 的 fast_memcpy 函数。还要考虑预期的使用模式,并注意现代 cpu 具有特殊的存储指令,可让您通知 cpu 是否需要读回正在写入的数据。使用指示您不会读回数据(因此不需要缓存)的指令对于大型 memcpy 操作可能是一个巨大的胜利。

【讨论】:

以上是关于如何提高 memcpy 的性能的主要内容,如果未能解决你的问题,请参考以下文章

在啥情况下我应该在 C++ 中使用 memcpy 而不是标准运算符?

memcpy 的内部实现是如何工作的?

linux下,设备DMA数据至0xF0000000,使用mmap映射,然后memcpy,效率相当低,16M需要400ms,有办法提高?

realloc 和 memcpy 是如何工作的?

如何摆脱 QIoDevice 中的 memcpy

如何让glog性能提高10倍