为什么memcpy()和memmove()比指针增量更快?

时间:2011-10-15 05:50:06

标签: c++ c loops

我正在将pSrc的N个字节复制到pDest。这可以在一个循环中完成:

for (int i = 0; i < N; i++)
    *pDest++ = *pSrc++

为什么这比memcpymemmove慢?他们用什么技巧来加快速度?

10 个答案:

答案 0 :(得分:111)

因为memcpy使用字指针而不是字节指针,所以memcpy实现通常用SIMD指令编写,这样就可以一次调整128位。

SIMD指令是汇编指令,可以对最长16个字节的向量中的每个元素执行相同的操作。这包括加载和存储指令。

答案 1 :(得分:78)

内存复制例程比通过指针(例如:

)的简单内存复制更加复杂和快捷
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处理器上,通过对齐目标指针而不是源指针,我获得了更好的性能。

为了进一步提高性能,可以进行一些循环展开,以便更多的处理器寄存器加载数据,这意味着加载/存储指令可以交错并通过附加指令隐藏其延迟(例如作为循环计数等)。这带来的好处因处理器而有很大差异,因为加载/存储指令延迟可能完全不同。

在此阶段,代码最终用汇编而不是C(或C ++)编写,因为您需要手动放置加载和存储指令,以获得延迟隐藏和吞吐量的最大好处。

通常,应在展开循环的一次迭代中复制整个缓存行数据。

这让我进入了下一步的改进,增加了预取。这些是特殊指令,告诉处理器的高速缓存系统将特定的内存部分加载到其高速缓存中。由于在发出指令和填充缓存行之间存在延迟,因此需要以这样的方式放置指令,以便数据在复制时可用,并且不会迟早。

这意味着将预取指令放在函数的开头以及主复制循环内。使用复制循环中间的预取指令来获取将在几个迭代时间内复制的数据。

我无法记住,但预取目标地址和源地址也可能是有益的。

<强>因素

影响内存复制速度的主要因素有:

  • 处理器,缓存和主内存之间的延迟。
  • 处理器缓存行的大小和结构。
  • 处理器的内存移动/复制指令(延迟,吞吐量,寄存器大小等)。

因此,如果您想编写一个高效且快速的内存应对程序,您需要了解很多有关您正在编写的处理器和体系结构的知识。可以说,除非你在某个嵌入式平台上编写,否则使用内置的内存复制程序要容易得多。

答案 2 :(得分:18)

memcpy可以一次复制多个字节,具体取决于计算机的体系结构。大多数现代计算机可以在单个处理器指令中使用32位或更多位。

来自one example implementation

    00026          * For speedy copying, optimize the common case where both pointers
    00027          * and the length are word-aligned, and copy word-at-a-time instead
    00028          * of byte-at-a-time. Otherwise, copy by bytes.

答案 3 :(得分:7)

您可以使用以下任何一种技术实现memcpy(),其中一些技术取决于您的体系结构以获得性能提升,并且它们都会比您的代码快得多:

  1. 使用更大的单位,例如32位字而不是字节。您也可以(或可能必须)处理对齐。您不能将32位字读取/写入奇数存储器位置,例如在某些平台上,而在其他平台上,您需要承担巨大的性能损失。要解决此问题,地址必须是可被4整除的单位。对于64位CPU,最高可达64位,甚至可以使用SIMD(单指令,多数据)指令({{3} },MMX等。)

  2. 您可以使用编译器可能无法从C优化的特殊CPU指令。例如,在80386上,您可以使用“rep”前缀指令+“movsb”指令来移动N个字节。将N放入计数寄存器。好的编译器会为你做这个,但你可能在一个缺乏良好编译器的平台上。请注意,该示例往往是速度的不良演示,但结合对齐+更大的单位指令,它可能比某些CPU上的其他所有内容都快。

  3. SSE - 分支在某些CPU上可能非常昂贵,因此展开循环可以降低分支数量。这也是结合SIMD指令和超大单位的好方法。

  4. 例如,Loop unrolling有一个memcpy实现,在那里击败最多(以非常小的数量)。如果您阅读了源代码,那么它将充满大量的内联汇编代码,这些代码可以完成上述三种技术,根据您运行的CPU选择哪种技术。

    注意,也可以在缓冲区中查找字节进行类似的优化。 strchr()和朋友通常会比你的手牌更快。对http://www.agner.org/optimize/#asmlib.NET尤其如此。例如,在.NET中,内置String.IndexOf()Java快得多,因为它使用了上述优化技术。

答案 4 :(得分:5)

简短回答:

  • 缓存填充
  • 尽可能使用字大小的传输而不是字节传输
  • SIMD magic

答案 5 :(得分:3)

像其他人一样说memcpy复制大于1字节的块。复制字大小的块更快。但是,大多数实现更进一步,并在循环之前运行几个MOV(字)指令。例如,每个循环复制8个字块的优点是循环本身很昂贵。这种技术将条件分支的数量减少了8倍,优化了巨型块的副本。

答案 6 :(得分:3)

我不知道它是否实际用于memcpy的任何实际实现,但我认为Duff's Device值得一提。

来自Wikipedia

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指针。它实现了稍微不同的操作:写入内存映射寄存器。有关详细信息,请参阅Wikipedia文章。

答案 7 :(得分:2)

答案很棒,但如果你仍然希望自己快速实现memcpy,那么有一篇关于快速memcpy的有趣博客帖子, Fast memcpy in C

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;
}

即便,优化内存访问也会更好。

答案 8 :(得分:1)

因为像许多库例程一样,它已针对您运行的体系结构进行了优化。其他人发布了可以使用的各种技术。

根据选择,使用库例程而不是自己滚动。这是DRY的一个变种,我称之为DRO(不要重复其他)。此外,库例程不太可能比您自己的实现错误。

我已经看到内存访问检查器抱怨内存或字符串缓冲区超出读取范围,这不是字大小的倍数。这是使用优化的结果。

答案 9 :(得分:0)

您可以查看memset,memcpy和memmove的MacOS实现。

在引导时,操作系统会确定它在哪个处理器上运行。它为每个受支持的处理器内置了经过专门优化的代码,并且在引导时将jmp指令存储到正确的代码中固定的只读/只读位置。

C memset,memcpy和memmove实现只是跳转到该固定位置。

根据memcpy和memmove的源和目标的对齐方式,实现使用不同的代码。他们显然使用了所有可用的向量功能。当您复制大量数据时,它们还使用非缓存变量,并具有减少页表等待的说明。这不仅仅是汇编代码,它是由对每种处理器体系结构非常了解的人编写的汇编代码。

英特尔还添加了汇编程序指令,可以使字符串操作更快。例如,一条指令支持strstr,该指令在一个周期内执行256字节比较。