strncpy/memcpy/memmove 是逐字节复制数据还是以其他有效方式复制数据?
Do strncpy/memcpy/memmove copy the data byte by byte or in another efficiently way?
我们知道,在 x86/x86_64 这样的多字节字计算机中,copy/move 一个字一个字地存储大量内存(每步 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.
Copies count bytes from the object pointed to by src to the object pointed to by dest. Both objects are reinterpreted as arrays of unsigned char.
NOTES
std::memcpy is meant to be the fastest library routine for memory-to-memory copy. It is usually more efficient than std::strcpy, which must scan the data it copies or std::memmove, which must take precautions to handle overlapping inputs.
Several C++ compilers transform suitable memory-copying loops to std::memcpy calls.
Where strict aliasing prohibits examining the same memory as values of two different types, std::memcpy may be used to convert the values.
所以复制数据应该是最快的方法了。但是请注意,在某些情况下行为未定义:
If the objects overlap, the behavior is undefined.
If either dest or src is a null pointer, the behavior is undefined, even if count is zero.
If the objects are potentially-overlapping or not TriviallyCopyable, the behavior of memcpy is not specified and may be undefined.
Does strcpy/strncpy copy the data byte by byte or in another efficiently way?
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,而是进行内联复制。您的编译器是否内置了标准库函数的定义,您可以在相应编译器的文档中找到。
一般来说,你不必过多考虑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 将检查指针的大小和对齐方式以使用最有效的内存复制策略(它可能开始将小块从一个字符一个字符复制到非常大的块例如使用 AVX512 指令的块)。这些检查以及对 memcpy 的任何调用成本。
此外,如果您正在寻找效率,则应该关注内存对齐。所以你可能想要声明缓冲区的对齐方式:
alignas(8) char buf_A[8];
这取决于您使用的编译器和您使用的 C 运行-time 库。在大多数情况下 string.h 函数,如 memcmp
、memcpy
、strcpu
、memset
等,使用汇编以 CPU 优化的方式实现。
您可以找到这些函数的 GNU libc 实现 for the AMD64 arhitecture。如您所见,它可能会使用 SSE 或 AVX 指令在每次迭代中复制 128 位和 512 位。 Microsoft 还将其 CRT 的源代码与 Visual Studio 捆绑在一起(主要是相同的方法,支持 MMX、SSE、AVX 循环)。
编译器也对此类函数进行了特殊优化,GCC 称它们为 builtins 其他编译器称它们为内部函数。 IE。编译器可以选择 - 调用一个库函数,或生成 CPU 特定的汇编代码,以针对当前上下文进行优化。例如,当 memcpy
的 N
参数是常量时,即 memcpy(dst, src, 128)
编译器可能生成内联汇编代码(类似于 mov 16,rcx cls rep stosq
),当它是变量时,即 [=18] =] - 编译器可能会插入对库函数的调用(类似于 call _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
看起来像:
- memcpy、memmove 和我自己的方法有相似的结果;
- strncpy 有什么神奇之处,所以它是最好的,甚至比 memcpy 还快?
很好笑吗?
我们知道,在 x86/x86_64 这样的多字节字计算机中,copy/move 一个字一个字地存储大量内存(每步 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.
Copies count bytes from the object pointed to by src to the object pointed to by dest. Both objects are reinterpreted as arrays of unsigned char.
NOTES
std::memcpy is meant to be the fastest library routine for memory-to-memory copy. It is usually more efficient than std::strcpy, which must scan the data it copies or std::memmove, which must take precautions to handle overlapping inputs.
Several C++ compilers transform suitable memory-copying loops to std::memcpy calls.
Where strict aliasing prohibits examining the same memory as values of two different types, std::memcpy may be used to convert the values.
所以复制数据应该是最快的方法了。但是请注意,在某些情况下行为未定义:
If the objects overlap, the behavior is undefined.
If either dest or src is a null pointer, the behavior is undefined, even if count is zero.
If the objects are potentially-overlapping or not TriviallyCopyable, the behavior of memcpy is not specified and may be undefined.
Does strcpy/strncpy copy the data byte by byte or in another efficiently way?
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,而是进行内联复制。您的编译器是否内置了标准库函数的定义,您可以在相应编译器的文档中找到。
一般来说,你不必过多考虑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 将检查指针的大小和对齐方式以使用最有效的内存复制策略(它可能开始将小块从一个字符一个字符复制到非常大的块例如使用 AVX512 指令的块)。这些检查以及对 memcpy 的任何调用成本。
此外,如果您正在寻找效率,则应该关注内存对齐。所以你可能想要声明缓冲区的对齐方式:
alignas(8) char buf_A[8];
这取决于您使用的编译器和您使用的 C 运行-time 库。在大多数情况下 string.h 函数,如 memcmp
、memcpy
、strcpu
、memset
等,使用汇编以 CPU 优化的方式实现。
您可以找到这些函数的 GNU libc 实现 for the AMD64 arhitecture。如您所见,它可能会使用 SSE 或 AVX 指令在每次迭代中复制 128 位和 512 位。 Microsoft 还将其 CRT 的源代码与 Visual Studio 捆绑在一起(主要是相同的方法,支持 MMX、SSE、AVX 循环)。
编译器也对此类函数进行了特殊优化,GCC 称它们为 builtins 其他编译器称它们为内部函数。 IE。编译器可以选择 - 调用一个库函数,或生成 CPU 特定的汇编代码,以针对当前上下文进行优化。例如,当 memcpy
的 N
参数是常量时,即 memcpy(dst, src, 128)
编译器可能生成内联汇编代码(类似于 mov 16,rcx cls rep stosq
),当它是变量时,即 [=18] =] - 编译器可能会插入对库函数的调用(类似于 call _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
看起来像:
- memcpy、memmove 和我自己的方法有相似的结果;
- strncpy 有什么神奇之处,所以它是最好的,甚至比 memcpy 还快?
很好笑吗?