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)
获得所需的内存性能有一些障碍:
带宽 - 数据从内存移动到CPU并再次返回的速度有限.根据维基百科的这篇文章,266MHz DDR3 RAM的上限约为17GB/s.现在,使用memcpy,您需要将其减半以获得最大传输速率,因为数据被读取然后写入.从您的基准测试结果来看,您似乎没有在系统中运行最快的RAM.如果你负担得起,升级主板/内存(并不便宜,英国的超频玩家目前拥有3x4GB的PC16000,价格为400英镑)
操作系统--Windows是一种抢占式多任务操作系统,因此您的进程通常会被暂停,以允许其他进程查看并执行操作.这将破坏你的缓存并阻止你的转移.在最坏的情况下,您的整个过程可以缓存到磁盘!
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完全脱离内存传输循环.
需要注意的一点是,您的过程(以及因此的性能memcpy()
)受到操作系统任务调度的影响 - 很难说这个因素在您的时间安排中有多大,因此很难控制.设备DMA操作不受此限制,因为一旦启动它就不会在CPU上运行.由于您的应用程序是一个实际的实时应用程序,您可能希望尝试Windows的进程/线程优先级设置(如果您还没有).请记住,您必须小心这一点,因为它可能会对其他进程(以及计算机上的用户体验)产生负面影响.
要记住的另一件事是操作系统内存虚拟化可能会产生影响 - 如果您要复制的内存页实际上没有物理RAM页面支持,memcpy()
操作将对操作系统出错以获得该物理支持地点.您的DMA页面很可能被锁定在物理内存中(因为它们必须用于DMA操作),因此源内存memcpy()
在这方面可能不是问题.您可以考虑使用Win32 VirtualAlloc()
API来确保memcpy()
提交的目标内存(我认为这VirtualAlloc()
是正确的API,但可能有一个我忘记的更好的 - 已经有一段时间了,因为我有一个需要做这样的事情).
最后,看看你是否可以使用Skizz解释的技术memcpy()
完全避免- 如果资源允许,这是你最好的选择.