【问题标题】:optimized memcpy优化的内存
【发布时间】:2010-11-15 14:56:46
【问题描述】:

在 C++ 中是否有更快的 memcpy() 替代方法?

【问题讨论】:

  • 如果有更快的方法,他们为什么不在memcpy 实现中使用它?
  • @MehrdadAfshari:memcpy 函数可以使用任意对齐的指针调用,指向任意 PODS 类型的对象,并且可以任意为其地址已暴露给外部代码的任何 PODS 对象设置别名。鉴于struct fnord a,b; void * volatile p=&a,*volatile q=&b;,我希望*((struct fnord*)p)=*((struct fnord*)q); 的性能比memcpy(p,q, sizeof (struct fnord)); 好得多,因为在前一种情况下,编译器可以合理地假设p 和q 将与struct fnord 对齐并且不会为其他任何东西设置别名,但在后一种情况它不能。

标签: c++ optimization memcpy


【解决方案1】:

首先,提个建议。假设编写您的标准库的人并不愚蠢。 如果有更快的方法来实现通用的 memcpy,他们就会这样做。

第二,是的,还有更好的选择。

  • 在 C++ 中,使用 std::copy 函数。它做同样的事情,但它 1)更安全,2)在某些情况下可能更快。它是一个模板,这意味着它可以专门用于特定类型,这可能比一般的 C memcpy 更快。
  • 或者,您可以利用您对您的具体情况的丰富知识。 memcpy 的实现者必须编写它,以便它在每个 情况下都表现良好。如果您有关于您需要它的情况的特定信息,您也许可以编写一个更快的版本。例如,您需要复制多少内存?它是如何对齐的?这可能允许您为 this 特定情况编写更有效的 memcpy。但在大多数其他情况下它不会那么好(如果它可以工作的话)

【讨论】:

  • 编译器实际上不太可能调用 memcpy 函数。我知道在 gcc 中它没有,但实际上用 i386 上的一条指令替换了 memcpy。
  • @PaulBiggar:对于 POD 类型,GCC 的 std::copy 将调用 memmove。如果您使用__restrict 提供别名提示,那么它将调用memcpy
【解决方案2】:

不太可能。您的编译器/标准库可能会有一个非常高效且量身定制的 memcpy 实现。而 memcpy 基本上是用于将内存的一部分复制到另一部分的最低 api。

如果您想进一步加快速度,请找到一种不需要任何内存复制的方法。

【讨论】:

  • 实际上,至少在 某些 情况下,至少有一种替代方案会更快,并且永远不会变慢。看我的回答。 :)
  • -1:众所周知,GCC 内置函数很糟糕(参见 Agner Fog 的基准测试)。好吧,也许它终于被修复了,但它说明了库不一定必须优化的一点。
  • @Bastien - 您能否提供指向 Agner Fog 基准的指针?我看到他的网站上有很多关于优化的信息,但我找不到任何明确的基准(除了一张比较一些 memcpy() 和 strlen() 例程的表,据我所知,内在支持例程已关闭)。
  • @Michael:查看 Agner 在 GCC 邮件列表中创建的讨论:gcc.gnu.org/ml/gcc/2008-07/msg00410.html
  • 感谢您的指点 - 我想知道 Fog 对本征 memcpy/memset 代码生成的测试是针对/调整到 generic/i386 还是使用了 -march 和/或 -mtune?在不久的将来可能会在我的机器上进行一些实验......
【解决方案3】:

优化专家 Agner Fog 已发布优化记忆功能:http://agner.org/optimize/#asmlib。不过它在 GPL 下。

前段时间,Agner 说这些函数应该替换 GCC 内置函数,因为它们要快得多。 不知道从那以后有没有。

【讨论】:

    【解决方案4】:

    这个非常相似的问题(关于memset())的答案也适用于这里。

    它基本上说编译器会为memcpy()/memset() 生成一些非常优化的代码 - 并根据对象的性质(大小、对齐方式等)生成不同的代码。

    记住,只有memcpy() C++ 中的 POD。

    【讨论】:

      【解决方案5】:

      为了找到或编写一个快速的内存复制例程,我们应该了解处理器是如何工作的。

      自 Intel Pentium Pro 以来的处理器执行“乱序执行”。如果指令没有依赖关系,它们可能会并行执行许多指令。但这仅是指令仅使用寄存器操作时的情况。如果它们与内存一起操作,则使用额外的 CPU 单元,称为“加载单元”(从内存中读取数据)和“存储单元”(将数据写入内存)。大多数 CPU 有两个加载单元和一个存储单元,即它们可以并行执行两条从内存读取的指令和一条写入内存的指令(同样,如果它们不相互影响)。这些单元的大小通常与最大寄存器大小相同——如果 CPU 有 XMM 寄存器 (SSE)——它是 16 字节,如果它有 YMM 寄存器 (AVX)——它是 32 字节,依此类推。所有读取或写入内存的指令都被转换为微操作(micro-ops),这些微操作(micro-ops)进入公共微操作池,并在那里等待加载和存储单元能够为它们服务。单个加载或存储单元一次只能服务一个微操作,无论它需要加载或存储的数据大小如何,无论是 1 字节还是 32 字节。

      因此,最快的内存复制将移入和移出具有最大大小的寄存器。对于支持 AVX 的处理器(但没有 AVX-512),复制内存的最快方法是重复以下序列,循环展开:

      vmovdqa     ymm0,ymmword ptr [rcx]
      vmovdqa     ymm1,ymmword ptr [rcx+20h]
      vmovdqa     ymmword ptr [rdx],ymm0
      vmovdqa     ymmword ptr [rdx+20h],ymm1
      

      hplbsh 之前发布的 Google 代码不是很好,因为它们在开始写回数据之前使用所有 8 个 xmm 寄存器来保存数据,而这并不是必需的——因为我们只有两个加载单元和一个存储单元。所以只有两个寄存器可以提供最好的结果。使用这么多寄存器并不能提高性能。

      内存复制例程还可以使用一些“高级”技术,例如“预取”来指示处理器提前将内存加载到缓存中和“非临时写入”(如果您正在复制非常大的内存块并且不需要立即读取输出缓冲区中的数据)、对齐写入与未对齐写入等。

      2013年发布的现代处理器,如果CPUID中有ERMS位,就有所谓的“增强rep movsb”,所以对于大内存拷贝,可能会用到“rep movsb”——拷贝会很快,甚至比使用 ymm 寄存器还要快,并且它可以正常使用缓存。然而,这条指令的启动成本非常高——大约 35 个周期,所以它只在大内存块上支付(然而,这可能会在未来的处理器中改变)。有关“rep movsb”的更多信息,请参阅https://*.com/a/43845229/6910868 上的“相对性能说明”部分,另请参阅https://*.com/a/43837564/6910868

      我希望您现在可以更轻松地选择或编写适合您的案例所需的最佳内存复制例程。

      您甚至可以保留标准的 memcpy/memmove,但根据需要获取自己的特殊 largememcpy()。

      【讨论】:

        【解决方案6】:

        我不确定使用默认的 memcpy 是否始终是最佳选择。我看过的大多数 memcpy 实现倾向于在开始时尝试对齐数据,然后进行对齐的副本。如果数据已经对齐,或者非常小,那么这是在浪费时间。

        有时使用专门的字副本、半字副本、字节副本 memcpy 是有益的,只要它对缓存没有太大的负面影响。

        此外,您可能希望更好地控制实际的分配算法。在游戏行业中,人们编写自己的内存分配例程是非常普遍的,无论工具链开发人员首先花费了多少精力来开发它。我见过的游戏几乎都倾向于使用Doug Lea's Malloc

        不过,一般来说,尝试优化 memcpy 是在浪费时间,因为毫无疑问,应用程序中有很多更简单的代码可以加快速度。

        【讨论】:

          【解决方案7】:

          取决于你想要做什么......如果它是一个足够大的 memcpy,并且你只是稀疏地写入副本,那么使用 MMAP_PRIVATE 来创建写时复制映射的 mmap 可能会更快.

          【讨论】:

          • 只有当地址空间处于不同的进程中时,写入时的复制才会起作用(回过头来说。)实际上,我认为你不必将它写入文件,如果你使用 MAP_ANONYMOUS 标志。
          • 不,内存映射也可以在两个内存位置之间使用
          • 这取决于“取决于你想要做什么”。如果说,他有 1Gb 的内存要复制,然后他可能只修改几 KB,但不知道提前哪个,那么做 mmap 只涉及到创建新的虚拟映射到相同的内存,原则上,它可能比复制 1Gb 更快。那么如果它们是写时复制的,那么只有被几KB修改所触及的页面才会真正被虚拟内存系统复制。所以,它会更快,并且取决于他在做什么。
          • 创建这样的 mmap 会很快,但它只会隐藏 memcpy 并在稍后写入 mmaped 内存时执行。而且这个拷贝会作为软件中断启动,非常慢(和memcpy相比)
          【解决方案8】:

          根据您的平台,可能会有特定的用例,例如,如果您知道源和目标与缓存行对齐,并且大小是缓存行大小的整数倍。一般来说,大多数编译器都会为 memcpy 生成相当优化的代码。

          【讨论】: