51

概括:

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;
}

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

4

8 回答 8

33

I have found a way to increase speed in this situation. I wrote a multi-threaded version of memcpy, splitting the area to be copied between threads. Here are some performance scaling numbers for a set block size, using the same timing code as found above. I had no idea that the performance, especially for this small size of block, would scale to this many threads. I suspect that this has something to do with the large number of memory controllers (16) on this machine.

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

I don't understand the huge performance jump between 3 and 4 threads. What would cause a jump like this?

I've included the memcpy code that I wrote below for other that may run into this same issue. Please note that there is no error checking in this code- this may need to be added for your application.

#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;
}
于 2010-11-24T17:28:34.490 回答
9

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

试试这个实现

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

于 2010-11-23T20:49:54.727 回答
5

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

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

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

于 2010-11-23T22:51:38.233 回答
4

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

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

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

  3. 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

或者获得更快的内存!

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

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

于 2010-11-23T21:36:56.307 回答
2

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

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

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

于 2010-11-23T20:54:52.323 回答
2

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

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

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

于 2010-11-23T21:26:47.800 回答
2

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

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

同样,您可以选择编写内联汇编代码吗?

于 2010-11-23T20:46:20.247 回答
1

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

于 2010-11-23T21:36:16.790 回答