图像处理中的快速memcpy技术?

39
我正在使用C进行图像处理,需要在内存中复制大块数据 - 源和目标永远不会重叠。在x86平台上,使用GCC(其中SSE,SSE2但不包括SSE3)最快的方法是什么?我预计解决方案将使用汇编或GCC内部函数实现。我找到了以下链接,但不知道它是否是最佳方法(作者还说它有一些错误):http://coding.derkeiler.com/Archive/Assembler/comp.lang.asm.x86/2006-02/msg00123.html编辑:请注意,必须进行复制,我无法避免复制数据(我可以解释原因,但我会省略解释:)

1
你能否编写代码,以便在第一次就不需要复制吗? - Ron
1
如果你能获得英特尔编译器,你可能会有更好的机会让优化器转换为矢量CPU指令。 - David Rodríguez - dribeas
2
看一下这个:http://software.intel.com/en-us/articles/memcpy-performance/ - David Rodríguez - dribeas
你知道你的编译器的memcpy()函数慢了多少吗?你能指定代码将在哪个处理器上运行吗?还有,操作系统是什么? - Clifford
我不知道什么对你最好,但就memcpy而言,有更快的版本。试试Agner Fog的asmlib(谷歌它)。它有汇编优化函数,如A_memcpy和A_memmove,比memcpy更快。 - user2088790
显示剩余2条评论
8个回答

50

感谢William Chan和Google。在Microsoft Visual Studio 2005中比memcpy快30-70%。

void X_aligned_memcpy_sse2(void* dest, const void* src, const unsigned long size)
{

  __asm
  {
    mov esi, src;    //src pointer
    mov edi, dest;   //dest pointer

    mov ebx, size;   //ebx is our counter 
    shr ebx, 7;      //divide by 128 (8 * 128bit registers)


    loop_copy:
      prefetchnta 128[ESI]; //SSE2 prefetch
      prefetchnta 160[ESI];
      prefetchnta 192[ESI];
      prefetchnta 224[ESI];

      movdqa xmm0, 0[ESI]; //move data from src to registers
      movdqa xmm1, 16[ESI];
      movdqa xmm2, 32[ESI];
      movdqa xmm3, 48[ESI];
      movdqa xmm4, 64[ESI];
      movdqa xmm5, 80[ESI];
      movdqa xmm6, 96[ESI];
      movdqa xmm7, 112[ESI];

      movntdq 0[EDI], xmm0; //move data from registers to dest
      movntdq 16[EDI], xmm1;
      movntdq 32[EDI], xmm2;
      movntdq 48[EDI], xmm3;
      movntdq 64[EDI], xmm4;
      movntdq 80[EDI], xmm5;
      movntdq 96[EDI], xmm6;
      movntdq 112[EDI], xmm7;

      add esi, 128;
      add edi, 128;
      dec ebx;

      jnz loop_copy; //loop please
    loop_copy_end:
  }
}

根据您的具体情况和您能够做出的任何假设,您可能能够进一步优化它。

您还可以查看memcpy源代码(memcpy.asm)并剥离其特殊情况处理。这可能有进一步的优化空间!


7
注意:这个内存复制的性能会在数据量和缓存大小上受到很大的影响。例如,预取和非暂态移动可能会使小一些的拷贝(适合 L2 缓存)的性能比普通的 movdqa 指令变差。 - Raphaël Saint-Pierre
2
栏杆:别忘了给他发邮件,告诉他你在项目中使用了他的代码 ;) [http://williamchan.ca/portfolio/assembly/ssememcpy/source/viewsource.php?id=readme.txt] - ardsrk
3
我记得首次在 AMD64 手册中读到这个代码。但是该代码在英特尔处理器上并不是最优的,因为它存在高速缓存银行别名问题。 - Gunther Piez

9
hapalibashi发布的SSE-Code是正确的方法。
如果您需要更高的性能,并且不介意编写设备驱动程序的漫长而曲折的道路:现在所有重要的平台都有DMA控制器,它能够比CPU代码更快地并行执行拷贝工作。
但这需要编写一个驱动程序。据我所知,没有任何一个大型操作系统会将此功能暴露给用户端,因为存在安全风险。
然而,如果您需要性能,这可能是值得的,因为没有任何代码可以超越专门设计用于此类工作的硬件。

3
我刚刚发布了一篇关于RAM带宽的答案。如果我说的是真的,那么我认为DMA引擎无法实现超出CPU能力范围的任务。我错过了什么吗? - Andrew Bainbridge

8
这个问题现在已经四年了,我有点惊讶没有人提到内存带宽。CPU-Z报告称,我的机器拥有PC3-10700 RAM。这种RAM的峰值带宽(也称传输速率、吞吐量等)为10700 MBytes/sec。我的机器中的CPU是i5-2430M CPU,其峰值睿频为3 GHz。
理论上,如果使用无限快的CPU和我的RAM,memcpy的速度可以达到5300 MBytes/sec,即10700的一半,因为memcpy必须从RAM读取然后写入。 (编辑:正如v.oddou指出的那样,这是一种简单的近似方法)。
另一方面,假设我们拥有无限快的RAM和一个现实的CPU,我们能够实现什么?让我们以我的3 GHz CPU为例。如果它可以每个周期执行32位读取和32位写入,则可以传输3e9 * 4 = 12000 MBytes/sec。对于现代CPU来说,这似乎很容易实现。我们已经可以看到,在CPU上运行的代码并不是真正的瓶颈。这是现代计算机具有数据缓存的原因之一。
我们可以通过基准测试memcpy在知道数据已缓存时所能实现的来测量CPU的真正性能。准确地做到这一点是棘手的。我制作了一个简单的应用程序,将随机数写入数组,将它们memcpy到另一个数组中,然后对复制的数据进行校验和。我在调试器中逐步执行代码,以确保聪明的编译器没有删除复制。更改数组的大小会改变缓存性能-小数组适合缓存,大数组则不太适合。我得到了以下结果:
40 KByte数组:16000 MBytes/sec 400 KByte数组:11000 MBytes/sec 4000 KByte数组:3100 MBytes/sec
显然,我的CPU可以每个周期读取和写入超过32位,因为16000比我上面理论计算的12000还要多。这意味着CPU甚至比我已经认为的更少成为瓶颈。我使用Visual Studio 2005,并进入标准memcpy实现,我可以看到它在我的机器上使用movqda指令。我猜测这可以每个周期读取和写入64位。
hapalibashi发布的好代码在我的机器上实现了4200 MBytes/sec的速度-比VS 2005实现快约40%。我猜测它之所以更快,是因为它使用prefetch指令来提高缓存性能。
总之,在CPU上运行的代码并不是瓶颈,调整该代码只会带来小幅改进。

你的思维过程很好。然而,你缺乏考虑营销内存数量,这些都是四倍频数字,与单通道速度不对应。而且这也是总线之前的速度,在NUMA模型中还有管理开销,这是Core i7 / Opteron所具有的。 - v.oddou

6
任何优化级别在-O1或以上的情况下,GCC将使用内置定义的函数,如memcpy - 使用正确的-march参数(-march=pentium4适用于您提到的功能集),它应该生成相当优化的特定于体系结构的内联代码。 我会进行基准测试并查看结果。

3

如果针对英特尔处理器,您可能会从IPP中受益。 如果您知道它将在Nvidia GPU上运行,也许您可以使用CUDA - 在这两种情况下,最好不要仅优化memcpy() - 它们提供了提高算法的机会在更高的级别上。 但是,它们都依赖于特定的硬件。


2
如果您使用的是Windows系统,可以使用DirectX API,它具有专门用于图形处理的GPU优化例程(速度有多快?您的CPU没有负担。在GPU运行时做其他事情)。
如果您想要跨平台,可以尝试使用OpenGL
不要尝试使用汇编语言,因为很可能无法胜任超过10年经验的专业库开发工程师的工作。

1
我需要在内存中执行它,也就是说,它不能在GPU上运行。 :) 此外,我不打算自己超越库函数(这就是我在这里提问的原因),但我相信stackoverflow上有人可以胜过这些库函数 :) 此外,库编写者通常受到可移植性要求的限制 - 就像我所说的,我只关心x86平台,因此可能还有更多的x86特定优化。 - horseyguy
+1,因为这是一个很好的第一条建议 - 即使它在Banister的情况下不适用。 - peterchen
3
我不确定这是好建议。一个典型的现代机器对于CPU和GPU来说具有大约相同的内存带宽。举例来说,许多流行的笔记本电脑使用英特尔HD图形,其使用与CPU相同的内存。CPU已经可以使内存总线饱和。对于memcpy,我期望在CPU或GPU上具有类似的性能。 - Andrew Bainbridge

1

这是一个老问题,但迄今为止还有两件事情没有被指出:

  1. 大多数编译器都有自己的版本 memcpy;由于 memcpy 已经被很好地定义并且是 C 标准的一部分,编译器不必使用随系统库提供的实现,它们可以自由地使用自己的实现。既然问题提到了“内置函数”,那么实际上,大多数情况下你在代码中写 memcpy 时,你实际上正在使用编译器内置函数,因为编译器会在内部使用它而不是真正调用 memcpy,这样甚至可以将其内联,从而消除任何函数调用开销。

  2. 我知道的大多数 memcpy 实现已经在内部使用 SSE2 等技术(如果可用),至少好的实现是这样的。Visual Studio 2005 的实现可能没有使用这个技术,但 GCC 已经使用了很长时间。当然,它们使用的取决于构建设置。它们只会使用所有 CPU 都支持的指令,所以请确保正确设置架构(例如 marchmtune),以及其他标志(例如启用对可选指令集的支持)。所有这些都会影响编译器在最终二进制文件中生成的 memcpy 代码。

所以,像往常一样,不要假设你可以比编译器或系统更聪明(它们可能为不同的CPU提供不同的memcpy实现),通过基准测试来证明!除非基准测试显示你手写的代码在实际中更快,否则最好让编译器和系统处理,因为它们会适应新的CPU,而系统可能会得到更新,自动使你的代码在未来运行更快,而你必须自己重新优化手写的代码,否则它永远不会变得更快,除非你自己发布更新。

更好的是,GCC不会为未知或大尺寸内联memcpy,所以它调用libc函数。例如,在Linux上,glibcmemcpy实现使用动态链接器钩子来解析符号,以便在动态链接时基于CPU检测将其解析为当前系统的最优选项,如支持快速256位非对齐向量加载/存储(例如Haswell及更高版本)的memmove_avx_unaligned_erms。https://codebrowser.dev/glibc/glibc/sysdeps/x86_64/multiarch/memmove-avx-unaligned-erms.S.html - Peter Cordes

-1
如果您可以访问DMA引擎,没有什么比它更快了。

1
你能指出在现代x86系统中可能会找到哪些特定的DMA引擎,它们可以比使用SSE或AVX的CPU核心更快地复制内存吗?PCIe 3.0具有x16链接仅能达到15.75 GB/s(参见https://en.wikipedia.org/wiki/PCI_Express#History_and_revisions),而双通道DDR4 2133 MT/s(例如来自2015年的Skylake CPU)提供理论带宽为34GB/s。因此,任何这样的DMA引擎都需要与CPU更紧密地连接。请注意,内存控制器内置于CPU中,因此在现代x86上,任何离芯片DMA引擎都必须通过CPU访问内存。 - Peter Cordes
一颗英特尔台式机/笔记本芯片的单个核心可以接近饱和DRAM带宽(不像许多核心的Xeon)。为什么Skylake在单线程内存吞吐量方面比Broadwell-E好得多?/增强的REP MOVSB用于memcpy。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接