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

时间:2016-02-13 20:48:46

标签: c++ performance loops memcpy strlen

我开始研究IT,现在我和朋友讨论这段代码效率是否低效。

// const char *pName
// char *m_pName = nullptr;

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

他声称例如memcopy会像上面的for循环那样做。我不知道这是不是真的,我不相信。

如果有更有效的方法或效率低下,请告诉我原因!

提前致谢!

8 个答案:

答案 0 :(得分:5)

充其量效率低下。最糟糕的是,它相当效率低下。

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

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

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

char *dest = m_pName;

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

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

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

答案 1 :(得分:5)

我看了actual g++ -O3 output for your code,看看它有多糟糕。

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循环计数器副本,这个副本在{0}和int i中都没有。 GCC。据推测,他们更难以解释size_t i可能产生无限循环的事实。

显然这太可怕了:

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

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

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

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

答案 2 :(得分:2)

此代码以各种方式混淆。

  1. 只需执行m_pName = pName;,因为您实际上并未复制字符串。 你只是指着你已经拥有的那个。

  2. 如果你想复制字符串m_pName = strdup(pName);就行了。

  3. 如果您已有存储空间,则会strcpymemcpy执行此操作。

  4. 无论如何,请将strlen排除在外。

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

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

答案 3 :(得分:1)

取决于效率的解释。我声称使用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风格字符串的结尾。

答案 4 :(得分:1)

是的,您的代码效率低下。您的代码采用所谓的“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)快得多。现在, about 尽可能高效。但是,memcpy调用仍然会快4-8倍,因为此代码一次复制1个字节,而memcpy将使用系统的字长。

答案 5 :(得分:1)

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

此外,strlen不太可能在循环条件之外进行优化,请参阅In C++, should I bother to cache variables, or let the compiler do the optimization? (Aliasing)

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

更快的是strcpy,因为它避免了strlen循环:

strcpy(m_pName, pName);

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

答案 6 :(得分:1)

对于像这样的简单操作,你几乎总是说出你的意思而已。

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

你们俩都不能赢得辩论。一个现代编译器已经看到了千种不同的memcpy()实现,并且它很有可能只是通过调用memcpy()或者使用它自己来识别你的代码并替换你的代码内联实现。

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

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

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

走得更远;因为memcpy()是一个基本的操作,所以即使硬件也可以识别编译器尝试做什么并实现自己的快捷方式,即使是编译器也没有#39; t know about。

不要担心对strlen()的多余调用。编译器也可能知道这一点。(编译器应该知道在某些情况下,但它似乎并不关心)编译器会看到所有。编译器知道所有。编译器在你睡觉的时候看着你。 信任编译器。

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

答案 7 :(得分:0)

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

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