如何提高 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 而不是标准运算符?
linux下,设备DMA数据至0xF0000000,使用mmap映射,然后memcpy,效率相当低,16M需要400ms,有办法提高?