我已经看到了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
以及src
和dest
的对齐方式和大小是8的倍数。
如果my_memcpy
确实会导致未定义的行为,我想知道memcpy
如何一次复制多个字节而不违反任何编译器假设。一个适用于x64的可行实现的示例将有所帮助。
使用库例程的建议无效。我实际上不是在写自己的memcpy
。我正在编写一个可以使用类似优化功能的函数,但C标准中不提供AFAIK。
答案 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
对指针进行限定。这告诉编译器可以假设dest
和src
指针不相同,或者它们重叠,这意味着编译器不需要为该场景添加检查或开销代码。
有趣的是,当我编写以下朴素的复制函数时:
#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
,也常常会造成明显的性能损失。