繁体   English   中英

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

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

我开始研究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循环计数器的mov ebx, ebp副本,在clang和gcc中都没有int isize_t i 据推测,他们更难以解释unsigned i可以产生无限循环的事实。

所以很明显这太可怕了:

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

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

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

memcpy可以比strcpy更高效,因为rep movs在Intel CPU上是高度优化的,尤其是。 IvB及以后。 但是,扫描字符串以找到合适的长度将始终比差异更大。 当您已经知道数据的长度时,请使用memcpy

充其量只是效率低下。 在最坏的情况下,这是非常低效的。

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

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

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

char *dest = m_pName;

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

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

当然,这与strcpy 这可能会或可能不会更有效 - 我当然看到它的情况。 由于你通常认为strcpy会被大量使用,所以花费更多的时间来优化它比在互联网上的一些随机人员在几分钟内输入答案更值得。

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

  1. 只需要做m_pName = pName; 因为你实际上并没有复制字符串。 你只是指着你已经拥有的那个。

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

  3. 如果你已经有存储, strcpymemcpy会这样做。

  4. 无论如何,将strlen从循环中取出。

  5. 这是担心性能的错误时间。 首先要做对。

  6. 如果你坚持担心表现,那就很难打败strcpy 更重要的是,你不必担心它是对的。

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

他声称例如memcopy会像上面的for循环那样做。

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


BTW来自您的评论代码:

// char *m_pName = nullptr;

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

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

为什么+1 因为你必须考虑放一个'\\0'表示c风格字符串的结尾。

是的,您的代码效率低下。 您的代码采用所谓的“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而是因为你在每次迭代时调用strlen strlen遍历整个数组,直到找到终止的零字节。

此外, 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()的多余调用。 编译器也可能知道这一点。 (编译器应该知道在某些情况下,但它似乎并不关心)编译器看到所有。 编译器知道所有。 编译器在你睡觉的时候看着你。 相信编译器。

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM