如何实现像memcpy()这样的快速复制功能?

时间:2019-06-27 09:10:07

标签: c performance memcpy strict-aliasing

我已经看到了memcpy()如何比单纯的逐字节复制更快的速度的一些答案。他们大多数人提出以下建议:

void *my_memcpy(void *dest, const void *src, size_t n) {
    uint64_t *d = dest;
    const uint64_t *s = src;
    n /= sizeof(uint64_t);

    while (n--)
        *d++ = *s++;

    return dest;
}
据我了解,

(如果我错了,请纠正我)会违反strict aliasing assumption并导致不确定的行为。为了简单起见,假设n以及srcdest的对齐方式和大小是8的倍数。

如果my_memcpy确实会导致未定义的行为,我想知道memcpy如何一次复制多个字节而不违反任何编译器假设。一个适用于x64的可行实现的示例将有所帮助。

使用库例程的建议无效。我实际上不是在写自己的memcpy。我正在编写一个可以使用类似优化功能的函数,但C标准中不提供AFAIK。

4 个答案:

答案 0 :(得分:4)

memcpy是一个特殊功能,编译器可以将其替换为内置版本,例如如果可以证明两个数组不重叠。

实际,快速的实现几乎总是使用汇编程序和特殊的内在函数(例如,带有SSSE3的glibc),但是其他libc实现也可以在C语言中实现(例如musl)。

答案 1 :(得分:3)

可移植的是,您应该基于对齐方式进行复制,而不必uint64_t。从理论上讲,您应该使用uint_fast8_t,但实际上,它显然是1字节大,在大多数系统上都是1字节对齐。如果不需要可移植性,则可以坚持使用uint64_t


下一个问题是,根据标准功能的要求,无论对齐如何,传递给memcpy的指针都不一定指向对齐的地址。因此,您必须执行以下操作:

size_t prealign = (uintptr_t)src % _Alignof(uint64_t);
if(prealign != 0)
{
  // copy bytes up to next aligned address
}

与目的地相同,对于数据末尾相同。


  我理解的

(如果我错了,请纠正我)会违反严格的别名假设并导致未定义的行为。

正确。因此,为了复制uint64_t块,您要么必须用内联汇编器编写代码,要么必须在编译时以非标准的方式禁用严格的别名,例如gcc -fno-strict-aliasing

与许多其他此类库函数一样,编译器将“真实”库memcpy视为特例。例如,memcpy(&foo, &bar, sizeof(int));将被翻译为单个mov指令,内嵌在调用方代码中,而根本不会调用memcpy


关于指针别名的另一条注释是,您应该像真实的memcpy一样restrict对指针进行限定。这告诉编译器可以假设destsrc指针不相同,或者它们重叠,这意味着编译器不需要为该场景添加检查或开销代码。

有趣的是,当我编写以下朴素的复制函数时:

#include <stdint.h>
#include <stddef.h>

void foocpy (void* dst, const void* src, size_t n)
{
  uint8_t* u8_dst = dst;
  const uint8_t* u8_src = src;

  for(size_t i=0; i<n; i++)
  {
    u8_dst[i] = u8_src[i];
  }
}

然后,编译器给了我一吨相当低效的机器代码。但是,如果我只是将restrict添加到两个指针,则整个函数将被替换为:

foocpy:
        test    rdx, rdx
        je      .L1
        jmp     memcpy
.L1:
        ret

这再次表明,内置memcpy被编译器视为特殊雪花。

答案 2 :(得分:0)

Tstenner已经详细说明了最重要的几点。

但是我要补充一点:如果您使用C进行编码,并且编译器比您聪明,它将注意到您编写了错误的memcpy版本,并将其替换为对实际版本的调用内置memcpy。例如:

#include <stdlib.h>

void *mymemcpy(void *restrict dest, const void * restrict src, size_t n) {
   char *csrc = (char *)src; 
   char *cdest = (char *)dest; 

   for (size_t i=0; i<n; i++) 
       cdest[i] = csrc[i]; 

   return dest;
}

使用GCC 9.1进行编译,结果汇编为

mymemcpy:
        test    rdx, rdx
        je      .L7
        sub     rsp, 8
        call    memcpy
        add     rsp, 8
        ret
.L7:
        mov     rax, rdi
        ret

那,鉴于您并不是想变得太聪明...

答案 3 :(得分:0)

有效利用特定目标体系结构的功能通常需要使用不可移植的代码,但是该标准的作者已明确认识到:

  

C代码不可移植。 [强调原始内容]尽管努力为程序员提供编写真正可移植程序的机会,但C89委员会不想强迫程序员进行可移植的编写,以排除使用C作为“高级汇编器”:编写的能力特定于机器的代码是C的优势之一。正是这一原理在很大程度上促使人们区分严格符合程序和符合程序(§4)。

分组优化需要使用流行的扩展,几乎所有实现都可以配置为支持。使用-fno-strict-aliasing标志在gcc和clang中启用此扩展可能会导致性能下降,除非代码在适当的时候使用了restrict限定符,但这应归咎于未能正确使用restrict。在正确使用-fno-strict-aliasing的代码中,restrict的性能损失很小,而即使不使用restrict,如果不使用-fno-strict-aliasing,也常常会造成明显的性能损失。