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.

来自cpp-reference

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 函数,如 memcmpmemcpystrcpumemset 等,使用汇编以 CPU 优化的方式实现。

您可以找到这些函数的 GNU libc 实现 for the AMD64 arhitecture。如您所见,它可能会使用 SSE 或 AVX 指令在每次迭代中复制 128 位和 512 位。 Microsoft 还将其 CRT 的源代码与 Visual Studio 捆绑在一起(主要是相同的方法,支持 MMX、SSE、AVX 循环)。

编译器也对此类函数进行了特殊优化,GCC 称它们为 builtins 其他编译器称它们为内部函数。 IE。编译器可以选择 - 调用一个库函数,或生成 CPU 特定的汇编代码,以针对当前上下文进行优化。例如,当 memcpyN 参数是常量时,即 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

看起来像:

  1. memcpy、memmove 和我自己的方法有相似的结果;
  2. strncpy 有什么神奇之处,所以它是最好的,甚至比 memcpy 还快?

很好笑吗?