当memcpy()比memmove()更快时,真正的重要案例是什么?

memcpy()memmove()之间的关键区别在于,当源和目标重叠时, memmove()将正常工作。 当缓冲区肯定不重叠时, memcpy()更可取,因为它可能更快。

困扰我的是这个潜在的 。 它是一个微优化还是当memcpy()更快时有真正重要的例子,所以我们真的需要使用memcpy()而不是到处都有memmove()

充其量,调用memcpy而不是memmove将保存指针比较和条件分支。 对于大型副本,这是完全无关紧要的。 如果您正在做许多小型副本,那么可能值得衡量差异; 这是唯一可以判断它是否重要的​​方法。

它绝对是一种微优化,但这并不意味着当你可以很容易地certificate它是安全的时候你不应该使用memcpy 。 过早的悲观情绪是许多邪恶的根源。

如果编译器无法推断出无法重叠,则至少有一个隐式分支可以向前或向后复制memmove() 。 这意味着如果不能优化memcpy()memmove()至少会被一个分支放慢,并且内联指令占用的任何额外空间都可以处理每种情况(如果可以内联)。

读取memcpy()memmove()eglibc-2.11.1代码可以确认这一点。 此外,在向后复制期间不可能进行页面复制,只有在没有重叠的情况下才能获得显着的加速。

总之,这意味着:如果可以保证区域不重叠,那么在memmove()选择memcpy() memmove()可以避免分支。 如果源和目标包含相应的页面对齐和页面大小的区域,并且不重叠,则某些体系结构可以为这些区域使用硬件加速副本,无论您是否调用了memmove()memcpy()

Update0

除了我上面列出的假设和观察之外,实际上还有一个区别。 从C99开始,这两个函数存在以下原型:

 void *memcpy(void * restrict s1, const void * restrict s2, size_t n); void *memmove(void * s1, const void * s2, size_t n); 

由于能够假设2个指针s1s2没有指向重叠的内存,因此memcpy直接C实现能够利用它来生成更高效的代码而无需求助于汇编程序,请参阅此处了解更多信息。 我确信memmove可以做到这一点,但是我需要在eglibc看到的那些上面进行额外的检查,这意味着对于这些函数的C实现,性能成本可能略高于单个分支。

好吧, memmove必须在源和目标重叠时向后复制, 并且源位于目标之前。 因此, memmove某些实现只是在源位于目标之前时向后复制,而不考虑这两个区域是否重叠。

memmove的高质量实现可以检测区域是否重叠,并在不执行时进行正向复制。 在这种情况下,与memcpy相比,唯一的额外开销就是重叠检查。

简单地说, memmove需要测试重叠然后做适当的事情; 使用memcpy ,一个断言没有重叠,因此不需要额外的测试。

话虽如此,我已经看到了具有完全相同的memcpymemmove代码的平台。

memcpy当然可能仅仅是对memmove的调用,在这种情况下使用memcpy没有任何好处。 另一方面,实现者可能很少使用memmove ,并且在C中使用最简单的一次一个字节循环来实现它,在这种情况下,它可能比优化的memcpy慢十倍。 正如其他人所说,最有可能的情况是memmove在检测到正向拷贝是可能时使用memcpy ,但是某些实现可能只是比较源地址和目标地址而不寻找重叠。

话虽如此,我建议永远不要使用memmove除非你在一个缓冲区内移动数据。 它可能不会慢,但话又说回来,那么为什么当你知道不需要memmove时冒险呢?

只需简化并始终使用memmove 。 一直都是正确的function比只有一半时间的function更好。

完全有可能在大多数实现中,memmove()函数调用的成本在定义两者行为的任何场景中都不会比memcpy()大得多。 但有两点尚未提及:

  1. 在一些实现中,地址重叠的确定可能是昂贵的。 在标准C中无法确定源和目标对象是否指向相同的内存分配区域,因此无法使用大于或小于运算符而不会自发地导致猫和狗彼此相处(或调用其他未定义的行为)。 任何实际实现都可能具有一些确定指针是否重叠的有效方法,但标准不要求存在这样的方法。 完全用可移植C编写的memmove()函数在许多平台上执行可能需要至少两倍的memcpy()也完全用便携式C编写。
  2. 允许实现在线扩展函数,这样做不会改变它们的语义。 在80×86编译器上,如果ESI和EDI寄存器没有发生任何重要事件,则memcpy(src,dest,1234)可以生成代码:
       mov esi,[src]
       mov edi,[dest]
       mov ecx,1234/4; 编译器可能会注意到它是一个常数
       CLD
       rep movsl
    

    这将采用相同数量的内联代码,但运行速度比:

      推[src]
      推[dest]
      推dword 1234
      打电话给_memcpy
    
       ...
    
     _memcpy:
      推ebp
       mov ebp,尤其是
       mov ecx,[ebp + numbytes]
      测试ecx,3; 看看它是否是四的倍数
       jz multiple_of_four
    
     multiple_of_four:
      推esi; 无法知道调用者是否需要保留此值
      推edi; 无法知道调用者是否需要保留此值
       mov esi,[ebp + src]
       mov edi,[ebp + dest]
       rep movsl
       pop edi
      流行esi
       RET  
    

相当多的编译器将使用memcpy()执行此类优化。 虽然在某些情况下memcpy的优化版本可能提供与memmove相同的语义,但我不知道有任何与memmove有关的内容。 例如,如果numbytes为20:

 ; 假设不需要eax,ebx,ecx,edx,esi和edi中的值
   mov esi,[src]
   mov eax,[esi]
   mov ebx,[esi + 4]
   mov ecx,[esi + 8]
   mov edx,[esi + 12]
   mov edi,[esi + 16]
   mov esi,[dest]
   mov [esi],eax
   mov [esi + 4],ebx
   mov [esi + 8],ecx
   mov [esi + 12],edx
   mov [esi + 16],edi

即使地址范围重叠,这也将正常工作,因为它有效地使整个区域的副本(在寄存器中)在其中任何一个被写入之前被移动。 理论上,编译器可以处理memmove(),看看是否将其作为memcpy()生成即使地址范围重叠也会安全的实现,并且在替换memcpy()实现的情况下调用_memmove安全。 不过,我不知道有没有做过这样的优化。