众所周知,在x86 / x86_64这样的多字节字计算机中,逐字复制/移动大量内存(每步4或8个字节)比逐字节更有效。
我很好奇strncpy / memcpy / memmove会做什么,以及它们如何处理内存字对齐。
char buf_A[8], buf_B[8];
// I often want to code as this
*(double*)buf_A = *(double*)buf_B;
//in stead of this
strcpy(buf_A, buf_B);
// but it worsen the readability of my codes.
通常,您不必过多考虑如何实现memcpy
或其他类似功能。除非你的分析证明你错了,否则你应该认为它们是有效的。
在实践中,它确实很好地优化了。参见例如以下测试代码:
#include <cstring>
void test(char (&a)[8], char (&b)[8])
{
std::memcpy(&a,&b,sizeof a);
}
使用命令g++ test.cpp -O3 -S -masm=intel
使用g ++ 7.3.0编译它我们can see以下汇编代码:
test(char (&) [8], char (&) [8]):
mov rax, QWORD PTR [rsi]
mov QWORD PTR [rdi], rax
ret
如您所见,副本不仅内联,而且还折叠为单个8字节读写。
在这种情况下,您可能更喜欢使用memcpy
,因为这相当于没有未定义行为的*(double*)buf_A = *(double*)buf_B;
。
你不应该担心调用memcpy
,因为默认情况下编译器假设对memcpy
的调用具有c库中定义的含义。因此,根据参数的类型和/或编译时副本大小的知识,编译器可以选择不调用c库函数并内联更适合的内存复制策略。在gcc上,您可以使用-fno-builtin
编译器选项禁用此行为:demo。
需要编译器替换memcpy调用,因为memcpy将检查指针的大小和对齐以使用最有效的内存复制策略(它可能开始像char一样通过char复制到非常大的块,使用AVX512指令例)。这些检查以及对memcpy的调用成本。
此外,如果您正在寻找效率,您应该关注内存对齐。所以你可能想要声明缓冲区的对齐方式:
alignas(8) char buf_A[8];
复制从src指向的对象到dest指向的对象的计数字节。这两个对象都被重新解释为unsigned char数组。
笔记
std :: memcpy意味着内存到内存复制的最快库例程。它通常比std :: strcpy更有效,它必须扫描它复制的数据或std :: memmove,它必须采取预防措施来处理重叠输入。
几个C ++编译器将合适的内存复制循环转换为std :: memcpy调用。
如果严格别名禁止检查与两种不同类型的值相同的内存,则std :: memcpy可用于转换值。
所以它应该是复制数据的最快方式。但请注意,有几种情况下行为未定义:
如果对象重叠,则行为未定义。
如果dest或src是空指针,则行为是未定义的,即使count为零也是如此。
如果对象可能重叠或不是TriviallyCopyable,则不指定memcpy的行为,并且可能未定义。
这取决于您使用的编译器和您正在使用的C运行时库。在大多数情况下,string.h函数如memcmp
,memcpy
,strcpu
,memset
等以CPU优化方式使用汇编实现。
您可以找到这些函数for the AMD64 arhitecture的GNU libc实现。如您所见,它可以使用SSE或AVX指令每次迭代复制128位和512位。 Microsoft还将其CRT的源代码与Visual Studio捆绑在一起(主要是相同的方法,支持MMX,SSE,AVX循环)。
编译器也对这类函数使用特殊优化,GCC称它们为builtins,其他编译器称它们为内在函数。即编译器可以选择 - 调用库函数,或生成最适合当前上下文的CPU特定汇编代码。例如,当N
的memcpy
参数是常数时,即memcpy(dst, src, 128)
编译器可能会生成内联汇编代码(类似于mov 16,rcx cls rep stosq
),当它是变量即memcpy(dst,src,bytes)
时 - 编译器可能会插入对库函数的调用(类似于call _memcpy
)
strcpy / strncpy是逐字节还是以其他方式有效地复制数据?
C ++和C标准没有具体说明strcpy / strncpy的实现方式。他们只描述行为。
有多个标准库实现,每个都选择如何实现它们的功能。可以使用memcpy实现这两者。标准也没有准确描述memcpy的实现,并且多个实现的存在也适用于它。
memcpy可以利用全文复制实现。关于如何实现memcpy的简短伪代码:
if len >= 2 * word size
copy bytes until destination pointer is aligned to word boundary
if len >= page size
copy entire pages using virtual address manipulation
copy entire words
copy the trailing bytes that are not aligned to word boundary
要了解特定标准库实现如何实现strcpy / strncpy / memcpy,您可以阅读标准库的源代码 - 如果您有权访问它。
更进一步,当在编译时知道长度时,编译器甚至可能选择不使用库memcpy,而是执行内联复制。无论您的编译器是否具有标准库函数的内置定义,您都可以在相应编译器的文档中找到。
我认为这个页面上的所有意见和建议都是合理的,但我决定尝试一些实验。
令我惊讶的是,最快的方法不是我们理论上预期的方法。
我尝试了一些代码如下。
#include <cstring>
#include <iostream>
#include <string>
#include <chrono>
using std::string;
using std::chrono::system_clock;
inline void mycopy( double* a, double* b, size_t s ) {
while ( s > 0 ) {
*a++ = *b++;
--s;
}
};
// to make sure that every bits have been changed
bool assertAllTrue( unsigned char* a, size_t s ) {
unsigned char v = 0xFF;
while ( s > 0 ) {
v &= *a++;
--s;
}
return v == 0xFF;
};
int main( int argc, char** argv ) {
alignas( 16 ) char bufA[512], bufB[512];
memset( bufB, 0xFF, 512 ); // to prevent strncpy from stoping prematurely
system_clock::time_point startT;
memset( bufA, 0, sizeof( bufA ) );
startT = system_clock::now();
for ( int i = 0; i < 1024 * 1024; ++i )
strncpy( bufA, bufB, sizeof( bufA ) );
std::cout << "strncpy:" << ( system_clock::now() - startT ).count()
<< ", AllTrue:" << std::boolalpha
<< assertAllTrue( ( unsigned char* )bufA, sizeof( bufA ) )
<< std::endl;
memset( bufA, 0, sizeof( bufA ) );
startT = system_clock::now();
for ( int i = 0; i < 1024 * 1024; ++i )
memcpy( bufA, bufB, sizeof( bufA ) );
std::cout << "memcpy:" << ( system_clock::now() - startT ).count()
<< ", AllTrue:" << std::boolalpha
<< assertAllTrue( ( unsigned char* )bufA, sizeof( bufA ) )
<< std::endl;
memset( bufA, 0, sizeof( bufA ) );
startT = system_clock::now();
for ( int i = 0; i < 1024 * 1024; ++i )
memmove( bufA, bufB, sizeof( bufA ) );
std::cout << "memmove:" << ( system_clock::now() - startT ).count()
<< ", AllTrue:" << std::boolalpha
<< assertAllTrue( ( unsigned char* )bufA, sizeof( bufA ) )
<< std::endl;
memset( bufA, 0, sizeof( bufA ) );
startT = system_clock::now();
for ( int i = 0; i < 1024 * 1024; ++i )
mycopy( ( double* )bufA, ( double* )bufB, sizeof( bufA ) / sizeof( double ) );
std::cout << "mycopy:" << ( system_clock::now() - startT ).count()
<< ", AllTrue:" << std::boolalpha
<< assertAllTrue( ( unsigned char* )bufA, sizeof( bufA ) )
<< std::endl;
return EXIT_SUCCESS;
}
结果(许多类似结果之一):
strncpy:52840919,AllTrue:true
memcpy:57630499,AllTrue:true
memmove:57536472,AllTrue:true
mycopy:57577863,AllTrue:true
看起来像:
有趣吗?