我正在从pSrc
复制N个字节到pDest
。这可以在单个循环中完成:
for (int i = 0; i < N; i++)
*pDest++ = *pSrc++
为什么这比memcpy
或memmove
慢?它们使用了什么技巧来加速?由于memcpy使用单词指针而不是字节指针,因此memcpy的实现通常使用SIMD指令编写,可以一次处理128位数据。
SIMD指令是汇编指令,可以对长度最长为16个字节的向量中的每个元素执行相同的操作。这包括装载和存储指令。
pDest
和 pSrc
不会发生别名的情况下会这样做。 - Dietrich Epp内存复制程序比简单的指针内存复制要复杂得多,速度也更快,例如:
void simple_memory_copy(void* dst, void* src, unsigned int bytes)
{
unsigned char* b_dst = (unsigned char*)dst;
unsigned char* b_src = (unsigned char*)src;
for (int i = 0; i < bytes; ++i)
*b_dst++ = *b_src++;
}
改进
第一个可以进行的改进是将指针之一对齐到字边界(所谓的字是指本机整数大小,通常为32位/4字节,但在新体系结构上可以是64位/8字节),并使用相应大小的移动/复制指令。这需要先进行字节到字节的复制,直到指针对齐。
void aligned_memory_copy(void* dst, void* src, unsigned int bytes)
{
unsigned char* b_dst = (unsigned char*)dst;
unsigned char* b_src = (unsigned char*)src;
// Copy bytes to align source pointer
while ((b_src & 0x3) != 0)
{
*b_dst++ = *b_src++;
bytes--;
}
unsigned int* w_dst = (unsigned int*)b_dst;
unsigned int* w_src = (unsigned int*)b_src;
while (bytes >= 4)
{
*w_dst++ = *w_src++;
bytes -= 4;
}
// Copy trailing bytes
if (bytes > 0)
{
b_dst = (unsigned char*)w_dst;
b_src = (unsigned char*)w_src;
while (bytes > 0)
{
*b_dst++ = *b_src++;
bytes--;
}
}
}
不同的架构在将源指针或目标指针适当对齐时会表现出不同的性能。例如,在 XScale 处理器上,通过对齐目标指针而不是源指针,我获得了更好的性能。b_src & 0x3
不会通过编译,因为在指针类型上不允许进行位运算。您必须首先将其转换为(u)intptr_t
。 - phuclvmemcpy
函数根据计算机的架构可以一次性复制多个字节。现代计算机中,单处理器指令可以同时处理32位或更多。
下面是一个示例实现:
00026 * 为了加快速度,当两个指针和长度都对齐时,会进行优化,使用每次复制一个字而不是一个字节的方式。否则,按字节复制。
memcpy()
,其中一些依赖于您的架构以获得性能提升,并且它们都比您的代码快得多:
使用更大的单元,例如32位字而不是字节。您还可以(或可能必须)在此处处理对齐。例如,在某些平台上,您不能读取/写入32位字到奇数内存位置,而在其他平台上,您需要支付巨大的性能惩罚。为了解决这个问题,地址必须是可被4整除的单位。对于64位CPU,您可以将其提高到64位,甚至使用SIMD(单指令,多数据)指令(MMX,SSE等)。
您可以使用特殊的CPU指令,您的编译器可能无法从C中进行优化。例如,在80386上,您可以使用“rep”前缀指令+“movsb”指令来移动由将N放置在计数寄存器中指定的N个字节。好的编译器会为您执行此操作,但您可能在缺乏良好编译器的平台上。请注意,该示例往往是速度不佳的演示,但与对齐+较大单元指令相结合,它可以比某些CPU上的几乎所有其他东西都更快。
展开循环 - 分支在某些CPU上可能非常昂贵,因此展开循环可以降低分支数量。这也是将SIMD指令和非常大的大小单位组合使用的好技术。
memcpy
实现,它击败了大多数其他实现(微不足道地)。如果您阅读源代码,它将充满大量内联汇编代码,执行上述三个技术中的哪一个取决于您正在运行的CPU。strchr()
等函数通常比自己编写的代码更快。这在 .NET 和 Java 中尤其如此。例如,在 .NET 中,内置的 String.IndexOf()
比甚至 Boyer–Moore 字符串搜索算法 更快,因为它使用了上述的优化技术。我不知道memcpy
是否在任何真实的实现中使用,但我认为达夫设备(Duff's Device)在这里值得一提。
send(to, from, count)
register short *to, *from;
register count;
{
register n = (count + 7) / 8;
switch(count % 8) {
case 0: do { *to = *from++;
case 7: *to = *from++;
case 6: *to = *from++;
case 5: *to = *from++;
case 4: *to = *from++;
case 3: *to = *from++;
case 2: *to = *from++;
case 1: *to = *from++;
} while(--n > 0);
}
}
memcpy
函数,因为它故意不会增加 to
指针。它实现了一个略微不同的操作:向内存映射寄存器写入数据。详情请参阅维基百科文章。*to
指的是一个内存映射寄存器,故意没有增加 - 请参阅链接的文章)。正如我所说,我的答案并没有试图提供一个高效的memcpy
,它只是提到了一种相当奇特的技术。 - NPE简短回答:
正如其他人所说,memcpy复制的是大于1字节的块。按字大小块进行复制速度更快。然而,大多数实现进一步运行几个MOV(字)指令后再执行循环。每次循环复制8个字大小的块的优势在于循环本身成本很高。这种技术将条件分支数量降低了8倍,优化了大块复制。
memcpy
,有一篇关于快速memcpy
的有趣博客文章:C语言中的快速memcpy
。请参考。void *memcpy(void* dest, const void* src, size_t count)
{
char* dst8 = (char*)dest;
char* src8 = (char*)src;
if (count & 1) {
dst8[0] = src8[0];
dst8 += 1;
src8 += 1;
}
count /= 2;
while (count--) {
dst8[0] = src8[0];
dst8[1] = src8[1];
dst8 += 2;
src8 += 2;
}
return dest;
}
因为像许多库例程一样,它已经针对您正在运行的架构进行了优化。其他人已经发布了可以使用的各种技术。
如果有选择的话,请使用库例程而不是自己编写。这是DRY的变体,我称之为DRO(不要重复别人)。此外,库例程比您自己的实现更不可能出错。
我曾看到内存访问检查器抱怨在非字大小倍数的内存或字符串缓冲区上进行越界读取。这是使用优化的结果。
0
计数到N-1
,绝不从1
计数到N
:-) - paxdiabloint
时,应该使用无符号类型如size_t
。 - Billy ONealmemcpy
或memmove
(取决于它们是否能确定指针可能别名)。 - David Schwartz