循环中的复制效率是否低于 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
副本,在 clang 和 gcc 中,int i
或 size_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
,而是因为你在每次迭代时调用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()
. 编译器可能也知道这一点。信任编译器。
哦,除了编译器可能无法捕获该空指针引用。 愚蠢的编译器!
这段代码以各种方式混淆。
-
只需
m_pName = pName;
,因为您实际上并没有复制字符串。你只是指向你已经得到的那个。 -
如果要复制字符串,
m_pName = strdup(pName);
会这样做。 -
如果您已经有存储空间,
strcpy
或memcpy
都可以。 -
无论如何,
strlen
出圈。 -
这是担心性能的错误时机。首先把它做好。
-
如果你坚持担心性能,很难击败
strcpy
。更重要的是,您不必担心它是否正确。
事实上,为什么你需要复制???(使用循环或内存(
如果你想复制一个内存块,这是一个不同的问题,但由于它是一个指针,你所需要的只是 &pName[0](这是数组第一个位置的地址(和 pName 的大小......就是这样。。。您可以通过递增第一个字节的地址来引用数组中的任何对象,并且您知道使用大小值的限制...为什么所有这些指针都???(让我知道这是否比理论辩论更多(
- 在提升multi_index容器中,是否定义了"default index"?
- 在C++STL中是否有Polyval(Matlab函数)等价物?
- 检查输入是否不是整数或数字
- 我是否应该在包含虚拟方法的类上使用'memcpy'?如果没有,如何替换它?
- memcpy是否取决于源指针和目标指针的类型
- 直接向工会而不是其特定成员之一发送memcpy数据是否安全
- memcpy 是否识别目的地的潜在大小?
- memcpy是否可以保留不同类型之间的数据
- 使用memcpy将浮点数组复制到uint8_t的数组是否有效
- 在已发布的结构中添加构造函数(用于 memcpy)是否安全?
- 如果从其他线程访问(读/写)src缓冲区,是否有可能从memcpy获得访问冲突
- 调用计数大于为 src 分配的内存的 memcpy 是否安全
- 使用 memcpy 移动非 POD C++对象是否总是调用未定义行为
- 是否允许 memcpy 的 src 和 dest 参数重叠
- 循环中的复制效率是否低于 memcpy()
- 按位操作是否比调用memcpy更快
- Memcpy/Memmove到工会成员,这是否设置了"活跃"成员?
- 如何访问 std::vector 的内部连续缓冲区,是否可以将其与 memcpy 等一起使用
- 当复制一些大于dst的内容时,memcpy()是否安全
- 是否允许:memcpy(dest, src, 0)