如何提高memcpy的性能

lee*_*ker 48 c cvi memcpy visual-studio memory-bandwidth

摘要:

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八核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;
}
Run Code Online (Sandbox Code Playgroud)

编辑:如果你有一个额外的五分钟,想参与,可以在您的机器上运行上面的代码,并发表您的时间作为评论?

lee*_*ker 32

在这种情况下,我找到了提高速度的方法.我编写了一个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
Run Code Online (Sandbox Code Playgroud)

我不明白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;
}
Run Code Online (Sandbox Code Playgroud)

  • @Robinson:绝对是一件好事.在过去几年中,我认为我已经得出结论,这最终成为NUMA性能问题. (4认同)
  • 相当旧的线程,但我想我会添加一些东西:缓存线一致性.仔细看看.可能解释了大规模的跳跃.当然,只是偶然.知道了这一点(Sutter写道),你可以制作一个智能memcpy,利用它来实现近乎完美的缩放. (3认同)
  • @leecbaker,4+线程的性能大幅提升来自缓存.当1,2或3个核心运行您的副本时,有另一个CPU运行其他内容或空闲.缓存几乎从不动态分配,因此整个CPU缓存不用于缓存读取和存储,这是当您生成4个以上的线程时的情况.此外,您的代码是错误的,只需查看计算每个线程的副本大小的代码. (2认同)

one*_*sse 9

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

试试这个实现.

是啊,并确保双方的源和目标对准到128位.如果你的源和目的地没有彼此对齐,你的memcpy()将不得不做一些严肃的魔术.:)


Ski*_*izz 5

获得所需的内存性能有一些障碍:

  1. 带宽 - 数据从内存移动到CPU并再次返回的速度有限.根据维基百科的这篇文章,266MHz DDR3 RAM的上限约为17GB/s.现在,使用memcpy,您需要将其减半以获得最大传输速率,因为数据被读取然后写入.从您的基准测试结果来看,您似乎没有在系统中运行最快的RAM.如果你负担得起,升级主板/内存(并不便宜,英国的超频玩家目前拥有3x4GB的PC16000,价格为400英镑)

  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
Run Code Online (Sandbox Code Playgroud)

代替:

load -> save -> load -> transform -> save
\-----------/
memcpy from
capture card
buffer to RAM
Run Code Online (Sandbox Code Playgroud)

或者获得更快的RAM!

编辑:另一个选择是处理数据源和PC之间的数据 - 你可以放一个DSP/FPGA吗?自定义硬件总是比通用CPU快.

另一个想法:我已经有一段时间了,因为我已经完成了任何高性能图形处理,但是你可以将数据DMA存入显卡然后再将其DMA掉吗?你甚至可以利用CUDA来完成一些处理.这将使CPU完全脱离内存传输循环.


Mic*_*urr 5

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

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

最后,看看你是否可以使用Skizz解释的技术memcpy()完全避免- 如果资源允许,这是你最好的选择.