CPU(基准测试方式)复制字符串的最有效方法是什么?
我是 c 的新手,我目前正在复制这样的字符串
char a[]="copy me";
char b[sizeof(a)];
for (size_t i = 0; i < sizeof(a); i++) {
b[i] = a[i];
}
printf("%s",b); // copy me
这是另一种选择,while循环比for循环快一点(我所听到的)
char a[]="copy me";
char b[sizeof(a)];
char c[sizeof(a)];
void copyAString (char *s, char *t)
{
while ( (*s++ = *t++) != ' ');
};
copyAString(b,a);
printf("%s",c);
当您可以使用标准函数(如memcpy
(当长度已知时)或strcpy
(当长度未知时)时,不要编写自己的复制循环。
现代编译器将这些视为"内置"函数,因此对于常量大小,可以将它们扩展到一些 asm 指令,而不是实际设置对库实现的调用,后者必须在大小上分支等等。 因此,如果您因为库函数调用短副本的开销而避免memcpy
,请不要担心,如果长度是编译时常量,则不会有一个。
但即使在未知/运行时可变长度的情况下,库函数通常也是用 asm 手写的优化版本,它比你在纯 C 中可以做的任何事情都要快得多(特别是对于中型到大型字符串),特别是对于没有未定义行为的 strcpy 读取缓冲区的末尾。
您的第一个代码块具有编译时常量大小(您可以使用sizeof
而不是strlen
)。 你的复制循环实际上会被现代编译器识别为固定大小的副本,并且(如果很大)变成对memcpy
的实际调用,否则通常进行类似的优化。
如何进行数组索引并不重要;优化编译器可以透视size_t索引或指针,并为目标平台提供良好的 asm。 请参阅此和此问答,了解代码实际编译方式的示例。 请记住,CPU直接运行asm,而不是C.
但是,此示例太小且过于简单,实际上无法用作基准测试。请参阅绩效评估的惯用方法?
您的第二种方式等效于隐式长度字符串的strcpy
。这更慢,因为它必须搜索终止的 0 字节,如果在内联和展开循环后的编译时不知道它。
特别是如果你像这样手动处理非常量字符串;现代 gcc/clang 无法自动矢量化循环,程序无法在第一次迭代之前计算行程计数。 也就是说,它们在像strlen和strcpy这样的搜索循环中失败了。
如果你实际上只是调用strcpy(dst, src)
,编译器将以某种有效的方式内联扩展它,或者发出对库函数的实际调用。 libc 函数使用手写的 asm 来高效地完成它,尤其是在像 x86 这样的 ISA 上,SIMD 可以提供帮助。 例如,对于x86-64,glibc的AVX2版本(https://code.woboq.org/userspace/glibc/sysdeps/x86_64/multiarch/strcpy-avx2.S.html)应该能够在Zen2和Skylake等主流CPU上为每个时钟周期复制32字节的中型副本,其中源和目标在缓存中很热。
似乎现代 GCC/clang不像他们识别 memcpy 等效循环那样将这种模式识别为 strcpy,因此如果您想对未知大小的 C 字符串进行高效复制,则需要使用实际strcpy
。 (或者更好的是,stpcpy
获取指向末尾的指针,以便您之后知道字符串长度,从而允许您使用显式长度的东西,而不是下一个函数也必须扫描字符串的长度。
一次用一个char
自己编写它最终将使用字节加载/存储指令,因此每个时钟周期最多可以使用 1 个字节。 (或者在 Ice Lake 上接近 2,可能在负载/宏融合测试/jz/存储的 5 宽前端上遇到瓶颈。 因此,对于具有运行时变量源的中型到大型副本来说,编译器无法删除循环,这是一场灾难。
(https://agner.org/optimize/x86 CPU 的性能。 其他架构大致相似,除了 SIMD 对 strcpy 的有用性。 如果没有 x86 的高效 SIMD>整数功能来分支 SIMD 比较结果,则可能需要使用通用整数位黑客,如 为什么 glibc 的 strlen 需要如此复杂才能快速运行?- 但请注意,这是glibc的便携式C回退,仅在少数没有人编写手动调整的ASM的平台上使用。
@0___________声称他们展开的一次char
循环比 glibcstrcpy
对于 1024 个字符的字符串更快,但这是不可信的,可能是错误的基准方法的结果。 (比如编译器优化击败了基准测试,或者页面错误开销或libc strcpy的惰性动态链接。
相关问答:
-
memcpy() 通常比 strcpy() 快吗? - 是的,尽管对于 x86 strcpy 上的大型副本几乎可以跟上;x86 SIMD 可以高效地检查整个区块中是否存在任何零字节。
-
比 memcpy 复制 0 终止字符串的更快方法
-
性能评估的惯用方法? - 微基准测试很难:您需要编译器来优化应该优化的部分,但仍会在基准测试循环中重复工作,而不仅仅是执行一次。
在 x86 和 x64 上读取同一页面中缓冲区的末尾是否安全? - 是的,以及内存保护在对齐页面中工作的所有其他 ISA。 (它在技术上仍然是C UB,但在asm中是安全的,因此用于库函数的手写asm可以100%安全地做到这一点。
效率:数组与指针
在 C 语言中,访问我的数组索引更快还是通过指针访问更快?
这可能不适合您的用例,但是当我复制图像数组时,我发现这段代码比memcpy快得多(我说的是>10倍)。可能有很多人会从中受益,所以我在这里发布它:
void fastMemcpy(void* Dest, void* Source, unsigned int nBytes)
{
assert(nBytes % 32 == 0);
assert((intptr_t(Dest) & 31) == 0);
assert((intptr_t(Source) & 31) == 0);
const __m256i* pSrc = reinterpret_cast<const __m256i*>(Source);
__m256i* pDest = reinterpret_cast<__m256i*>(Dest);
int64_t nVects = nBytes / sizeof(*pSrc);
for (; nVects > 0; nVects--, pSrc++, pDest++)
{
const __m256i loaded = _mm256_stream_load_si256(pSrc);
_mm256_stream_si256(pDest, loaded);
}
_mm_sfence();
}
这利用了内部函数,因此包括
通常,复制字符串的最有效方法是手动展开循环,以最大程度地减少所需的操作数。
例:
char *mystrcpy(char *restrict dest, const char * restrict src)
{
char *saveddest = dest;
while(1)
{
if(!(*dest++ = *src++)) break;
if(!(*dest++ = *src++)) break;
if(!(*dest++ = *src++)) break;
if(!(*dest++ = *src++)) break;
if(!(*dest++ = *src++)) break;
if(!(*dest++ = *src++)) break;
if(!(*dest++ = *src++)) break;
if(!(*dest++ = *src++)) break;
if(!(*dest++ = *src++)) break;
if(!(*dest++ = *src++)) break;
if(!(*dest++ = *src++)) break;
if(!(*dest++ = *src++)) break;
if(!(*dest++ = *src++)) break;
if(!(*dest++ = *src++)) break;
if(!(*dest++ = *src++)) break;
if(!(*dest++ = *src++)) break;
}
return saveddest;
}
https://godbolt.org/z/q3vYeWzab
glibc
实现使用非常相似的方法。