如何提高memcpy的性能

52

概要:

在我的系统中,memcpy似乎无法在实际或测试应用程序中传输超过2GB /秒的数据。我该怎么做才能获得更快的内存到内存复制速度?

完整细节:

作为一个数据捕获应用程序的一部分(使用一些专门的硬件),我需要将大约3 GB /秒的数据从临时缓冲区复制到主内存。为了获取数据,我向硬件驱动程序提供一系列缓冲区(每个缓冲区为2MB)。硬件会将数据DMA到每个缓冲区中,然后在每个缓冲区满时通知我的程序。我的程序清空缓冲区(memcpy到另一个较大的RAM块),并将处理后的缓冲区重新提交给卡填充数据。我在使用memcpy移动数据时遇到了速度不够快的问题。看起来,在我运行的硬件上,内存到内存复制速度应该足够支持3GB /秒。Lavalys EVEREST 给出的内存复制基准测试结果为9337MB / sec,但即使在简单测试程序中,我也无法获得与此接近的速度。

我通过在缓冲区处理代码中添加/删除memcpy调用来确定了性能问题。如果没有memcpy,我可以以全数据速率运行-大约3GB / sec。启用memcpy后,我被限制在约550Mb / sec(使用当前编译器)。

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

Visual C++ 2010:1900 MB / sec

NI CVI 2009:550 MB / sec

虽然我并不奇怪CVI比Visual Studio慢得多,但是我对memcpy的性能这么低感到惊讶。虽然我不确定这是否可以直接比较,但这比EVEREST基准带宽低得多。虽然我不需要那么高的性能水平,但最小速度为3GB /秒是必要的。毕竟,标准库实现不能比EVEREST使用的任何东西差这么多!

在这种情况下,我该怎么做才能使memcpy更快?


硬件详细信息: AMD Magny Cours- 4x八核 128 GB DDR3 Windows Server 2003 Enterprise 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;
}

编辑:如果您有额外的五分钟时间并希望做出贡献,您可以在自己的计算机上运行上述代码,并在评论中发布您的时间。


1
我的笔记本显示相同的内存带宽。但是一个快速设计的 SSE2/4 算法并没有显著提高性能(只有轻微的改善)。 - Christopher
1
更多的SSE代码测试只能使VC2010中的memcpy算法速度提高60 MB/sec。Core-i5笔记本电脑峰值约为2,224 GB/sec(这个数字不应该翻倍吗?我们同时写入和读取这个数字,所以大约是4.4 GB/sec...)。要么是我忽略了某些东西,要么就是你真的必须“不复制”你的数据。 - Christopher
1
请查看onemasse的答案(William Chan的SSE2 ASM memcpy实现)-使用memcpy和CopyMemory,我获得了1.8GB / s的速度。使用William的实现,我获得了3.54GB / s的速度(几乎是两倍!)。这是在具有800MHz 2通道DDR2的Core2Duo Wolfdale上进行的。 - Zach Saw
根据我的下面的回答,我刚想到从采集卡传输数据将消耗部分可用于CPU的内存带宽。我认为你会失去大约33%(memcpy = 读/写,采集卡= 写/读/写),所以你的应用内memcpy速度将比基准memcpy慢。 - Skizz
Macbook Retina Pro核心i7 2.6GHz(通过Bootcamp运行Win 7 x64):8474 MB/秒。编译器是Embarcadero C++Builder 2010。 - Roddy
有趣的话题。Core i7-4770 3.4GHz,2x8GB DDR3 1600MHz CL9。VS 2013 x64 rls build。5个测量值。Win8:约13GB/s,Win7:约11GB/s。 - waldez
8个回答

36

我找到了一种提高速度的方法。我编写了一个多线程版本的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

我不理解为什么使用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;
}

4
这是一个相当古老的帖子,但我想补充一些内容:缓存行一致性。查一下这个概念,可能会解释这个巨大的性能提升。当然,这只是偶然发生的。如果你了解这个(Sutter写过相关内容),你可以编写一个智能memcpy函数,利用它来实现几乎完美的扩展性。 - Robinson
4
@Robinson:肯定值得关注。在过去的几年中,我认为我得出结论,这最终成为了一个非一致性内存访问(NUMA)性能问题。 - leecbaker
1
就我个人而言,我在我的i5-2430M笔记本电脑上尝试了您的代码。线程数几乎没有什么区别。1、2、4和8个线程基本上是相同的速度。我发现最快的memcpy来自hapalibashi在这个问题上的回答:https://dev59.com/XnI-5IYBdhLWcg3wta1q。 - Andrew Bainbridge
2
@leecbaker,4个或更多线程的大幅度性能提升源于缓存。当只有1、2或3个核心运行您的副本时,还有另一个CPU正在运行其他任务或处于空闲状态。缓存几乎从不动态分配,因此在生成4个或更多线程时,整个CPU缓存都被用于缓存读取和存储,而在较少线程时则会出现这种情况。此外,您的代码肯定是错误的,请查看每个线程计算复制大小的代码。 - sgupta

9

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

尝试this implementation

是的,请确保目标都对齐到128位。如果你的源和目标彼此不对齐,那么你的memcpy()将需要进行一些严重的操作。 :)


1
你需要将源和目标/都/对齐到16字节(而不是32位)。William Chan的代码使用movdqa(a表示对齐)。请参见http://siyobik.info/index.php?module=x86&id=183。您还应该为最后一点性能分配缓存对齐内存。 - Zach Saw
是的,我说“至少”。但是如果您想进行基于向量的I/O操作,将数据对齐到128位当然是有意义的。我已经更正了我的答案。 - onemasse
啊,我以为你指的是你在链接中发布的实现。 - Zach Saw

5

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

需要记住的另一件事是操作系统内存虚拟化可能会对此产生影响——如果你要复制的内存页面实际上没有被物理RAM页面支持,那么memcpy()操作将向操作系统发出故障请求以放置物理支持。由于DMA操作必须进行,因此你的DMA页面很可能被锁定在物理内存中,因此memcpy()的源内存在这方面可能不是问题。你可以考虑使用Win32 VirtualAlloc() API来确保你的memcpy()目标内存已提交(我认为VirtualAlloc()是正确的API,但我可能忘记了更好的API——我已经有一段时间没有需要做这样的事情了)。
最后,请尝试使用 Skizz解释的技术完全避免使用memcpy()——如果资源允许的话,这是最好的选择。

要锁定页面,可以使用SetProcessWorkingSetSize和VirtualLock。 - Skizz

4
您在获取所需内存性能方面存在一些障碍:
1. 带宽 - 数据从内存到CPU,再从CPU回到内存的移动速度是有限制的。根据维基百科文章,266MHz DDR3 RAM的上限大约为17GB/s。现在,对于memcpy,您需要将此数字减半以获得最大传输速率,因为数据会被读取然后写入。从您的基准测试结果来看,似乎您的系统中没有运行最快的RAM。如果您有能力负担,可以升级主板/内存(这不便宜,在英国,Overclockers目前售价为400英镑的3x4GB PC16000)。
2. 操作系统 - Windows是一种抢占式多任务操作系统,因此每隔一段时间您的进程将被暂停,以允许其他进程进行查看和操作。这将破坏您的缓存并使传输停止。在最坏的情况下,整个进程可能会被缓存到磁盘上!
3. CPU - 被移动的数据经过了很长的路程:RAM -> L2缓存 -> L1缓存 -> 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

或者使用更快的内存条!

编辑:另一个选择是在数据源和个人电脑之间处理数据 - 你能否在其中放置 DSP/FPGA?自定义硬件总是比通用 CPU 更快。

另一个想法:我已经有一段时间没有做过高性能图形方面的工作了,但是您可以将数据 DMA 到显卡中,然后再将其 DMA 出来吗?您甚至可以利用 CUDA 来完成部分处理。这将完全将 CPU 推出内存传输循环。


Skizz,我在数据进来时并没有进行任何数学处理-只是将其复制到另一个缓冲区,所以另一个DMA或DSP/FPGA的使用对此无济于事。数据确实通过双缓冲系统进来-实际上是一个包含4个或更多缓冲区的队列,并且被复制到一个静态长缓冲区(10GB+)。 - leecbaker
关于更快的RAM:目前系统有16个PC3-10600通道,其理论峰值传输速率为10.7GB/s(每个通道)。虽然我意识到我无法接近这个峰值评级,但我认为我仍然应该在RAM的硬件性能方面有一些余地。 - leecbaker
@leecbaker:那数据发生了什么? - Skizz
数据被收集并存储在RAM中,当所有数据都被收集后,整个批次将被处理。收集是我关注的性能敏感部分。 - leecbaker

2
首先,您需要检查内存是否对齐在16字节边界上,否则会受到惩罚。这是最重要的事情。
如果您不需要符合标准的解决方案,可以使用一些编译器特定扩展(例如memcpy64)来检查是否有所改善(请查阅您的编译器文档以了解是否有可用的内容)。事实上,memcpy必须能够处理单字节复制,但如果您没有此限制,则每次移动4或8个字节要快得多。
再次,请问您是否可以编写内联汇编代码?

内联汇编是一种选择,但其他评论者指出它并没有带来显著的改进。此外,我刚刚验证了所有内存块都是16字节对齐的。 - leecbaker
你能在 Stack Overflow 上发布一下你的编译器生成的汇编代码吗? - Simone

2
也许您可以更详细地说明一下如何处理更大的内存区域?
在您的应用程序中,是否有可能仅传递缓冲区的所有权,而不是复制它?这将完全消除问题。
或者您是否仅使用memcpy进行复制?也许您正在使用更大的内存区域从捕获的数据构建连续流?特别是如果您一次处理一个字符,那么您可能能够达成妥协。例如,可能可以调整处理代码以适应表示为“缓冲区数组”的流,而不是“连续内存区域”。

在数据捕获期间,我没有对存储缓冲区中的数据进行任何操作。它将在后期转储到文件中。 - leecbaker
1
能否直接捕获到更大的内存区域?您可以按顺序建立缓冲指针数组,然后将它们写出。(您甚至可以尝试使用WriteFileGather来获取向量IO,但它有一些相当严格的对齐要求。) - Stéphan Kochen

2
你可以使用SSE2寄存器编写更好的memcpy实现。VC2010中的版本已经这样做了。因此问题更多地是,如果你正在处理对齐内存,则需要一些理解。也许你可以比VC 2010的版本做得更好,但这确实需要一些理解。
PS:你可以通过反向调用将缓冲区传递给用户模式程序,以完全避免复制。

1
我推荐你阅读的一个资源是MPlayer的fast_memcpy函数。此外,考虑到预期的使用模式,并注意到现代CPU具有特殊的存储指令,可以让你告诉CPU是否需要读回你正在写入的数据。使用指示你不会读回数据(因此它不需要被缓存)的指令,对于大型memcpy操作来说可能是一个巨大的优势。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接