我开始学IT,现在正在和一个 friend 讨论这段代码是不是低效。
// const char *pName
// char *m_pName = nullptr;
for (int i = 0; i < strlen(pName); i++)
m_pName[i] = pName[i];
他声称例如 memcopy 会像上面的 for 循环一样做同样的事情。我想知道这是不是真的,我不相信。
如果有更有效的方法或者这种方法效率低下,请告诉我原因!
提前致谢!
最佳答案
我看了一下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
循环计数器拷贝,这是 int i< 所没有的
或 size_t i
,在 clang 和 gcc 中。据推测,他们很难解释 unsigned i
可能产生无限循环这一事实。
很明显这很可怕:
- 对复制的每个字节调用
strlen
- 一次复制一个字节
- 每次循环都重新加载
m_pName
(可以通过将其加载到本地来避免)。
使用 strcpy
可以避免所有这些问题,因为 strlen
可以假设它的 src 和 dst 不重叠。使用 strlen
+ memcpy
除非你想自己了解 strlen
。如果 strcpy
的最有效实现是 strlen
+ memcpy
,库函数将在内部执行此操作。否则,它会做一些更有效率的事情,比如 glibc's hand-written SSE2 strcpy
for x86-64 . (有一个 SSSE3 version ,但它在 Intel SnB 上实际上更慢,而 glibc 足够聪明,不会使用它。)即使是 SSE2 版本也可能展开得比它应该的多(在微基准测试上很好,但会污染指令缓存, uop-cache 和 branch-predictor 缓存(当用作实际代码的一小部分时)。大部分复制是在 16B block 中完成的,启动/清理部分有 64 位、32 位和更小的 block 。
当然,使用 strcpy
也可以避免错误,例如忘记在目标中存储尾随 '\0'
字符。如果您的输入字符串可能很大,使用 int
作为循环计数器(而不是 size_t
)也是一个错误。使用 strncpy
通常更好,因为您通常知道目标缓冲区的大小,但不知道源缓冲区的大小。
memcpy
比 strcpy
更高效,因为 rep movs
在 Intel CPU 上进行了高度优化,尤其是。 IvB 及更高版本。然而,首先扫描字符串以找到正确的长度总是比差值花费更多。当您已经知道数据的长度时,请使用 memcpy
。
关于c++ - 循环复制是否比 memcpy() 效率低?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35385157/