![](/img/trans.png)
[英]Would initializing a variable in (e.g) a loop be less efficient than just using one already defined?
[英]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 i
或size_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
会被大量使用,所以花费更多的时间来优化它比在互联网上的一些随机人员在几分钟内输入答案更值得。
这段代码以各种方式混淆。
只需要做m_pName = pName;
因为你实际上并没有复制字符串。 你只是指着你已经拥有的那个。
如果要复制字符串m_pName = strdup(pName);
会做的。
如果你已经有存储, strcpy
或memcpy
会这样做。
无论如何,将strlen
从循环中取出。
这是担心性能的错误时间。 首先要做对。
如果你坚持担心表现,那就很难打败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()
或者使用它自己的内联实现来识别你的代码并替换你的代码。
它知道哪一个最适合您的情况。 或者至少它可能比你知道得更好。 当你第二次猜测你冒着编译器无法识别它的风险而你的版本比收集的聪明技巧更糟糕时,编译器和/或库知道。
如果要运行自己的代码而不是库代码,必须考虑以下几个注意事项:
rep movsb
这样的专用指令? 它有它们但是它们表现更差吗? 走得更远; 因为memcpy()
是一个基本的操作,即使是硬件也可能会识别编译器尝试做什么并实现自己的快捷方式,即使是编译器也不知道。
不要担心对 strlen()
的多余调用。编译器也可能知道这一点。 (编译器应该知道在某些情况下,但它似乎并不关心)编译器看到所有。 编译器知道所有。 编译器在你睡觉的时候看着你。 相信编译器。
哦,除了编译器可能没有捕获该空指针引用。 愚蠢的编译器!
事实上,为什么你需要复制? (使用循环或memcpy)
如果你想复制一个内存块,这是一个不同的问题,但由于它的指针只需要&pName [0](这是数组第一个位置的地址)和sizeof pName ...就是这样...你可以通过递增第一个字节的地址来引用数组中的任何对象,你知道使用大小值的限制...为什么要有所有这些指针?(让我知道是否还有比理论辩论更多的内容)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.