循环中的复制效率是否低于 memcpy()

Is copying in a loop less efficient than memcpy()?

本文关键字:memcpy 是否 复制 效率 循环      更新时间:2023-10-16

我开始学习IT,我现在正在和一个朋友讨论这段代码是否效率低下。

// const char *pName
// char *m_pName = nullptr;
for (int i = 0; i < strlen(pName); i++)
    m_pName[i] = pName[i];

他声称,例如memcopy可以像上面的for循环一样做同样的事情。我想知道这是不是真的,我不相信。

如果有更有效的方法,或者效率低下,请告诉我为什么!

提前感谢!

我查看了您的代码的实际g++ -O3输出,看看它有多糟糕。

char*可以别名任何东西,所以即使是__restrict__ GNU C++扩展也不能帮助编译器将strlen提升到循环之外。

我以为它会被吊起来,并期望这里的主要效率低下只是一次字节的复制循环。 但是不,它真的和其他答案所暗示的一样糟糕。 m_pName甚至每次都必须重新加载,因为混叠规则允许m_pName[i] this->m_pName混叠。 编译器不能假定存储到m_pName[i]不会更改类成员变量、src 字符串或其他任何内容。

#include <string.h>
class foo {
   char *__restrict__ m_pName = nullptr;
   void set_name(const char *__restrict__ pName);
   void alloc_name(size_t sz) { m_pName = new char[sz]; }
};
// g++ will only emit a non-inline copy of the function if there's a non-inline definition.
void foo::set_name(const char * __restrict__ pName)
{
    // char* can alias anything, including &m_pName, so the loop has to reload the pointer every time
    //char *__restrict__ dst = m_pName;  // a local avoids the reload of m_pName, but still can't hoist strlen
    #define dst m_pName
    for (unsigned int i = 0; i < strlen(pName); i++)
        dst[i] = pName[i];
}

编译到这个 asm (g++ -O3 for x86-64, SysV ABI(:

...
.L7:
        movzx   edx, BYTE PTR [rbp+0+rbx]      ; byte load from src.  clang uses mov al, byte ..., instead of movzx.  The difference is debatable.
        mov     rax, QWORD PTR [r12]           ; reload this->m_pName    
        mov     BYTE PTR [rax+rbx], dl         ; byte store
        add     rbx, 1
.L3:                                 ; first iteration entry point
        mov     rdi, rbp                       ; function arg for strlen
        call    strlen
        cmp     rbx, rax
        jb      .L7               ; compare-and-branch (unsigned)

使用unsigned int循环计数器会引入循环计数器的额外mov ebx, ebp副本,在 clang 和 gcc 中,int isize_t i 都无法获得。 据推测,他们很难解释unsigned i可能产生无限循环的事实。

所以很明显,这很可怕:

  • 对复制的每个字节进行strlen调用
  • 一次复制一个字节
  • 每次通过循环重新加载m_pName(可以通过将其加载到本地来避免(。

使用 strcpy 可以避免所有这些问题,因为允许strlen假设它的 src 和 dst 不重叠。 不要使用 strlen + memcpy,除非您自己想了解strlen。 如果strcpy最有效的实现是 strlen + memcpy ,库函数将在内部执行此操作。 否则,它将做一些更有效的事情,比如glibc手写的x86-64的SSE2 strcpy。 (有一个SSSE3版本,但它实际上在Intel SnB上更慢,而且glibc足够聪明,不会使用它。 即使是 SSE2 版本也可能比它应该展开的更多(在微基准测试上很棒,但当用作实际代码的一小部分时会污染指令缓存、uop 缓存和分支预测器缓存(。 大部分复制以 16B 块完成,在启动/清理部分中包含 64 位、32 位和更小的块。

当然,使用 strcpy 也可以避免诸如忘记在目标中存储尾随''字符之类的错误。 如果您的输入字符串可能很大,则使用 int 作为循环计数器(而不是 size_t (也是一个错误。 使用 strncpy 通常更好,因为您通常知道 dest 缓冲区的大小,但不知道 src 的大小。

memcpy可以比 strcpy 更有效,因为rep movs在英特尔 CPU 上进行了高度优化,尤其是 IvB 及更高版本。 但是,首先扫描字符串以找到正确的长度总是比差异花费更多。 当您已经知道数据的长度时,请使用memcpy

充其量它的效率很低。在最坏的情况下,它的效率非常低

在良好的情况下,编译器认识到它可以将调用提升到循环之外strlen。在这种情况下,您最终遍历输入字符串一次以计算长度,然后再次复制到目标。

在不好的情况下,编译器调用循环的每次迭代strlen在这种情况下,复杂性变为二次而不是线性。

至于如何有效地做到这一点,我倾向于这样的事情:

char *dest = m_pName;
for (char const *in = pName; *in; ++in)
    *dest++ = *in;
*dest++ = '';

这只遍历输入一次,因此它的速度可能是第一个的两倍左右,即使在更好的情况下也是如此(在二次情况下,它可以快很多倍,具体取决于字符串的长度(。

当然,这与strcpy做的事情几乎相同。这可能更有效,也可能不会更有效 - 我当然见过这样的情况。由于您通常认为strcpy将被大量使用,因此花更多的时间来优化它是值得的,而不是在互联网上随机输入几分钟的答案。

是的,你的代码效率低下。你的代码需要所谓的"O(n^2("时间。为什么?你的循环中有 strlen(( 调用,所以你的代码在每个循环中都会重新计算字符串的长度。您可以通过这样做来使其更快:

unsigned int len = strlen(pName);
for (int i = 0; i < len; i++)
    m_pName[i] = pName[i];

现在,你只计算一次字符串长度,所以这段代码需要"O(n("时间,这比 O(n^2( 快得多。现在,这几乎是您可以获得的效率。但是,memcpy 调用仍然会快 4-8 倍,因为此代码一次复制 1 个字节,而 memcpy 将使用系统的字长。

取决于对效率的解释。我声称使用memcpy()strcpy()更有效率,因为您不会每次需要副本时都编写这样的循环。

他声称,例如memcopy可以像上面的for循环一样做同样的事情。

嗯,不完全一样。可能是因为memcpy()占用一次大小,而strlen(pName)可能会在每次循环迭代中调用。因此,从潜在的性能效率考虑memcpy()会更好。


顺便说一句,来自您注释的代码:

// char *m_pName = nullptr;

像这样初始化会导致未定义的行为,而无需为m_pName分配内存:

char *m_pName = new char[strlen(pName) + 1];

为什么+1?因为您必须考虑放置一个指示 c 样式字符串末尾的''

是的,它效率低下,不是因为你使用循环而不是memcpy,而是因为你在每次迭代时调用strlenstrlen遍历整个数组,直到找到终止的零字节。

此外,strlen不太可能在循环条件之外进行优化,请参阅C++,我应该费心缓存变量,还是让编译器进行优化?(别名(。

所以memcpy(m_pName, pName, strlen(pName))确实会更快。

更快的

strcpy,因为它避免了strlen循环:

strcpy(m_pName, pName);

strcpy与@JerryCoffin答案中的循环相同。

对于像这样的简单操作,您几乎总是应该说出您的意思,仅此而已。

在这种情况下,如果您的意思是strcpy()那么您应该这么说,因为strcpy()会复制终止 NUL 字符,而该循环不会。

你们谁都无法赢得辩论。 现代编译器已经看到了一千种不同的memcpy()实现,它很有可能会识别你的代码,并用对memcpy()的调用或它自己的内联实现来替换你的代码。

它知道哪一个最适合您的情况。 或者至少它可能比你更了解。 当你事后猜测你冒着编译器无法识别它的风险,并且你的版本比编译器和/或库知道的收集的聪明技巧更糟糕时。

如果要运行自己的代码而不是库代码,则必须正确考虑以下几个注意事项:

  • 有效的最大读/写块大小是多少(很少是字节(。
  • 对于多大的循环长度范围,值得麻烦地预先对齐读取和写入,以便可以复制更大的块?
  • 对齐
  • 读取、对齐写入、不执行任何操作,还是对齐两者并在算术中执行排列以补偿更好?
  • 如何使用 SIMD 寄存器? 它们更快吗?
  • 在第一次写入之前应执行多少次读取? 需要多少寄存器文件才能实现最有效的突发访问?
  • 是否应包含预取指令?
    • 领先多远?
    • 多久一次?
    • 循环是否需要额外的复杂性来避免在末端预加载?
  • 这些决策中有多少可以在运行时解决而不会造成太多开销? 测试是否会导致分支预测失败?
  • 内联会有所帮助,还是只是浪费 icache?
  • 循环代码是否受益于缓存行对齐? 是否需要将其紧密打包到单个缓存行中? 同一缓存行中的其他指令是否有约束?
  • 目标 CPU 是否有像rep movsb这样的专用指令,性能更好? 它有它们,但它们的表现更差吗?

更进一步;因为memcpy()是一个如此基本的操作,所以即使是硬件也会识别编译器试图做什么,并实现自己的快捷方式,甚至编译器也不知道。

不用担心多余的电话strlen(). 编译器可能也知道这一点。(编译器在某些情况下应该知道,但它似乎并不关心( 编译器可以看到所有内容。 编译器知道一切。 编译器会在您睡觉时监视您。 信任编译器。

哦,除了编译器可能无法捕获该空指针引用。 愚蠢的编译器!

这段代码以各种方式混淆。

  1. 只需m_pName = pName;,因为您实际上并没有复制字符串。你只是指向你已经得到的那个。

  2. 如果要复制字符串,m_pName = strdup(pName);会这样做。

  3. 如果您已经有存储空间,strcpymemcpy都可以。

  4. 无论如何,strlen出圈。

  5. 这是担心性能的错误时机。首先把它做好。

  6. 如果你坚持担心性能,很难击败strcpy。更重要的是,您不必担心它是否正确。

事实上,为什么你需要复制???(使用循环或内存(

如果你想复制一个内存块,这是一个不同的问题,但由于它是一个指针,你所需要的只是 &pName[0](这是数组第一个位置的地址(和 pName 的大小......就是这样。。。您可以通过递增第一个字节的地址来引用数组中的任何对象,并且您知道使用大小值的限制...为什么所有这些指针都???(让我知道这是否比理论辩论更多(