当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个指针s1
和s2
没有指向重叠的内存,因此memcpy
直接C实现能够利用它来生成更高效的代码而无需求助于汇编程序,请参阅此处了解更多信息。 我确信memmove
可以做到这一点,但是我需要在eglibc
看到的那些上面进行额外的检查,这意味着对于这些函数的C实现,性能成本可能略高于单个分支。
好吧, memmove
必须在源和目标重叠时向后复制, 并且源位于目标之前。 因此, memmove
某些实现只是在源位于目标之前时向后复制,而不考虑这两个区域是否重叠。
memmove
的高质量实现可以检测区域是否重叠,并在不执行时进行正向复制。 在这种情况下,与memcpy
相比,唯一的额外开销就是重叠检查。
简单地说, memmove
需要测试重叠然后做适当的事情; 使用memcpy
,一个断言没有重叠,因此不需要额外的测试。
话虽如此,我已经看到了具有完全相同的memcpy
和memmove
代码的平台。
memcpy
当然可能仅仅是对memmove
的调用,在这种情况下使用memcpy
没有任何好处。 另一方面,实现者可能很少使用memmove
,并且在C中使用最简单的一次一个字节循环来实现它,在这种情况下,它可能比优化的memcpy
慢十倍。 正如其他人所说,最有可能的情况是memmove
在检测到正向拷贝是可能时使用memcpy
,但是某些实现可能只是比较源地址和目标地址而不寻找重叠。
话虽如此,我建议永远不要使用memmove
除非你在一个缓冲区内移动数据。 它可能不会慢,但话又说回来,那么为什么当你知道不需要memmove
时冒险呢?
只需简化并始终使用memmove
。 一直都是正确的function比只有一半时间的function更好。
完全有可能在大多数实现中,memmove()函数调用的成本在定义两者行为的任何场景中都不会比memcpy()大得多。 但有两点尚未提及:
- 在一些实现中,地址重叠的确定可能是昂贵的。 在标准C中无法确定源和目标对象是否指向相同的内存分配区域,因此无法使用大于或小于运算符而不会自发地导致猫和狗彼此相处(或调用其他未定义的行为)。 任何实际实现都可能具有一些确定指针是否重叠的有效方法,但标准不要求存在这样的方法。 完全用可移植C编写的memmove()函数在许多平台上执行可能需要至少两倍的memcpy()也完全用便携式C编写。
- 允许实现在线扩展函数,这样做不会改变它们的语义。 在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安全。 不过,我不知道有没有做过这样的优化。