如何提高memcpy的性能

概要:

memcpy似乎无法在真实或testing应用程序中在我的系统上传输超过2GB /秒。 我能做些什么来获得更快的内存到内存拷贝?

全部细节:

作为数据采集应用程序的一部分(使用一些专用硬件),我需要将大约3 GB /秒的临时缓冲区拷贝到主内存中。 为了获取数据,我给硬件驱动程序提供了一系列缓冲区(每个2MB)。 硬件DMA将数据发送到每个缓冲区,然后在每个缓冲区满时通知我的程序。 我的程序清空缓冲区(memcpy到另一个较大的RAM块),并将处理后的缓冲区重新发送到卡再次填充。 我有memcpy移动数据足够快的问题。 看起来内存到内存的拷贝速度应该足够快,可以在我运行的硬件上支持3GB /秒的速度。 Lavalys EVEREST给我提供了9337MB / sec的内存复制基准testing结果,但是即使在一个简单的testing程序中,我也无法使用memcpy获得接近这些速度的任何地方。

我通过添加/删除缓冲区处理代码中的memcpy调用来隔离性能问题。 没有memcpy,我可以运行完整的数据速率 – 约3GB /秒。 在启用memcpy的情况下,我仅限于550Mb / sec(使用当前的编译器)。

为了在我的系统上对memcpy进行基准testing,我已经编写了一个单独的testing程序,在一些数据块上调用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基准带宽要低得多。 虽然我不需要相当的性能水平,但至less需要3GB /秒。 标准库的实现肯定不会比EVEREST所使用的更糟糕!

在这种情况下,我能做些什么来加快memcpy的速度?


硬件细节:AMD Magny Cours-四核八核128 GB DDR3 Windows Server 2003 Enterprise X64

testing程序:

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

编辑:如果你有额外的五分钟,想要贡献,你可以在你的机器上运行上面的代码,并张贴您的时间作为评论?

在这种情况下,我find了一种提高速度的方法。 我写了一个memcpy的multithreading版本,拆分线程之间要复制的区域。 下面是一些设置块大小的性能缩放数字,使用与上面相同的时间码。 我不知道,特别是对于这个小块的性能,可以扩展到这么multithreading。 我怀疑这与这台机器上的大量内存控制器(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; } 

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

试试这个实现 。

是的,并确保源和目标都alignment到128位。 如果你的源和目标不相互alignment你的memcpy()将不得不做一些严重的魔术。 🙂

获得所需的内存性能有几个障碍:

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

  2. 操作系统 – Windows是一个抢先式的多任务操作系统,所以每隔一段时间你的进程将被暂停,以允许其他进程看看和做的东西。 这将会打破你的caching,并阻止你的转移。 在最坏的情况下,你的整个过程可以caching到磁盘!

  3. CPU – 正在移动的数据还有很长的路要走:RAM – > L2 Cache – > L1 Cache – > CPU – > L1 – > L2 – > RAM。 甚至可能有一个三级caching。 如果你想涉及CPU,你真的想要加载L2,同时复制L1。 不幸的是,现代CPU可以比L1加载所花费的时间快得多。 CPU有一个内存控制器,在这种情况下,你可以顺序地将数据stream送入CPU,但是你仍然会遇到问题。

当然,更快捷的做法是不做。 捕获的数据可以写入RAM中的任何位置,也可以是在固定位置使用的缓冲区。 如果你可以把它写在任何地方,那么你完全不需要memcpy。 如果它是固定的,你可以处理数据并使用双缓冲区types系统吗? 也就是说,开始捕获数据,当它半满时,开始处理前一半的数据。 当缓冲区满时,开始将捕获的数据写入开始处理下半部分。 这要求algorithm能够比捕捉卡产生的数据更快地处理数据。 它还假定数据在处理后被丢弃。 实际上,这是作为复制过程一部分的转换的一个memcpy,所以你有:

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

代替:

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

或者得到更快的RAM!

编辑:另一个select是处理数据源和PC之间的数据 – 你可以把一个DSP / FPGA在那里呢? 自定义硬件总是比通用CPU快。

另一个想法:我已经做了很长一段时间,因为我已经做了任何高性能的graphics的东西,但你能DMA的数据进入显卡,然后再DMA的呢? 你甚至可以利用CUDA做一些处理。 这将使CPU完全脱离内存传输循环。

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

另外需要注意的是,操作系统内存虚拟化可能会对此产生影响 – 如果要复制的内存页实际上并不是由物理内存页支持的,则memcpy()操作将会对操作系统造成故障物理支持到位。 你的DMA页面可能被locking在物理内存中(因为它们必须用于DMA操作),所以memcpy()的源内存在这方面可能不是问题。 您可能会考虑使用Win32 VirtualAlloc() API来确保memcpy()目标内存被提交(我认为VirtualAlloc()是适合这个的API,但是可能会有更好的一个,我忘了 – 这是已经有一段时间了,因为我需要做这样的事情)。

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

首先,你需要检查内存是否在16字节的边界上alignment,否则你会受到惩罚。 这是最重要的事情。

如果你不需要一个兼容标准的解决scheme,你可以通过使用一些编译器特定的扩展(如memcpy64来检查是否改进(如果有可用的东西,请检查你的编译器文档)。 事实上, memcpy必须能够处理单字节拷贝,但是如果你没有这个限制,一次移动4或8个字节会快得多。

再说一次,你是否可以select内联汇编代码呢?

也许你可以解释一些关于你如何处理更大的内存区域?

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

还是你用memcpy不仅仅是复制? 也许你正在使用更大的内存区域来从你所捕获的数据中build立连续的数据stream? 特别是如果你一次只处理一个angular色,那么你可能会半途而废。 例如,可以调整处理代码以适应表示为“缓冲区数组”的stream,而不是“连续的内存区域”。

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

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

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

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

Interesting Posts