【问题标题】:c - what is the most efficient way to copying a string?c - 复制字符串的最有效方法是什么?
【发布时间】:2022-01-20 22:03:10
【问题描述】:

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++) != '\0');
};

copyAString(b,a);

printf("%s",c);

【问题讨论】:

  • 对于编译时常数大小,几乎总是memcpy。当以较小的固定大小调用时,编译器将内联它。当然,优化编译器也将识别此复制循环并将其替换为对 memcpy 的实际调用或它的内联扩展,无论您如何进行数组索引。不过,这个例子太小太简单,实际上不能用作基准。 Idiomatic way of performance evaluation?
  • re:您的编辑:第二种方式是 strcpy 用于隐式长度字符串。这比较慢,因为它必须搜索终止的 0 字节,如果在内联和展开循环后编译时不知道它。 (如果幸运的话,它将优化循环以调用 libc 中的 strcpy,它使用手写 asm 来高效地执行此操作,尤其是在 SIMD 可以提供帮助的 x86 等 ISA 上。)
  • while 循环与 for 循环的效率属于“沉没成本”类别——节省的成本不会随字符串长度而变化。正如 Peter Cordes 所说,memcpy() 很难改进,但很有可能您的编译器会在可能的地方使用它(即使您没有明确调用它)。但是,如果您确实直接调用 memcpy(),请确保包含空终止符。
  • @mzimmers:作为数组初始值设定项的字符串文字确实包含终止 0 字节。而sizeof() 是数组的整个大小,包括它。所以char a[]="copy me"; 的第一个示例确实复制了终止符,就像 strcpy 版本一样。
  • @PeterCordes:没有争论。我并没有特别提到这个例子。只是在字符串上使用 memcpy 时需要记住的一件事。

标签: c string loops for-loop benchmarking


【解决方案1】:

这可能不适合您的用例,但是当我复制图像数组时,我发现这段代码比 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();
}

这利用了内在函数,所以包括 。流命令绕过 CPU 缓存,似乎在速度上有很大的不同。对于更大的数组,您还可以使用多个线程,从而进一步提高性能。

【讨论】:

  • 适用于大量数据,至少几个 KiB,并且只有在它被从缓存中驱逐之前你不打算再次读取它时反正。 (因此更常用于至少与 L3 缓存一样大的数据,尽管对于您也不打算重新读取的多个较小的副本可能有意义。避免驱逐 other 内容也很有价值如果你不打算很快重读;这就是非临时 NT 提示的意思。)如果你这样做是为了一份小副本,你重新阅读它,你'd 迫使该阅读器在缓存中丢失。
  • 您在什么硬件上进行了测试?我很惊讶这比 memcpy 快 10 倍。如果幸运的话,3 倍听起来很合理,但 10 倍听起来更像是测量/实验错误。请参阅Enhanced REP MOVSB for memcpy re:no-RFO 缓存写入协议(就像 NT 商店也使用),以及英特尔服务器芯片(更高延迟互连)之间的差异如何在这里产生影响。
  • “客户端”芯片(如 Skylake 台式机或笔记本电脑)上的多线程 memcpy 几乎没有差异;单个内核几乎可以使 DRAM 控制器饱和。但这在大型 Xeon 上非常不同,尤其是 Skylake 以及更高延迟的网格互连和更低的单线程最大带宽,其中聚合 B/W 随内核数量而扩展(或者换句话说,您需要所有内核来最大化 DRAM)。 Why is Skylake so much better than Broadwell-E for single-threaded memory throughput?
  • 我有一个 Intel Core i5 8259u,我正在复制一个大小为 1600x1000x4 浮点数的图像(用于 OpenGL)。我没有确切的时间。我通过 OpenGL 中的 Compute Shader 在 Mandelbrot 计算中使用了它。计算(包括复制)的时间从 80 毫秒减少到 10 毫秒——仅使用 fastMemCpy。所以计算着色器需要小于 10 毫秒的时间。这意味着 memcpy 占用了 >70ms 的时间,因此 fastMemCpy
  • 您是否使用它直接向设备(视频)内存复制或从设备(视频)内存复制?标准库 memcpy 有多糟糕?我猜您使用的是带有 MSVC 的 Windows,因为您使用了 intrin.h 而不是可移植的 Intel 标头名称 immintrin.h,但是如果他们的标准 memcpy 对于普通的 mem-mem 副本来说太糟糕了,我会感到惊讶。如果您在循环中对 just this 或 memcpy 进行微基准测试,则在预热以排除页面错误之后,这可能是相同的速度,或者如果 MSVC 的 @987654326 更快(如 1.5 倍或 2 倍) @ 不使用 NT 存储来存储大副本。
【解决方案2】:

当您可以使用标准函数,例如 memcpy(当长度已知时)或 strcpy(当长度未知时)时,不要编写您自己的复制循环。

现代编译器将这些视为“内置”函数,因此对于常量大小,可以将它们扩展为一些 asm 指令,而不是实际设置对库实现的调用,后者必须根据大小进行分支等等。因此,如果您因为库函数调用短副本的开销而避免使用memcpy,请不要担心,如果长度是编译时常量,则不会有。

但即使在未知/运行时可变长度的情况下,库函数通常也会是用 asm 手写的优化版本,比纯 C 中可以做的任何事情都要快得多(尤其是对于中大型字符串),尤其是对于 strcpy 没有未定义行为的读取缓冲区末尾。

您的第一个代码块具有编译时常数大小(您可以使用sizeof 而不是strlen)。您的复制循环实际上会被现代编译器识别为固定大小的副本,并且(如果很大)变成对memcpy 的实际调用,否则通常会进行类似的优化。

如何进行数组索引并不重要;优化编译器可以看穿 size_t 索引或指针,并为目标平台制作好的 asm。 请参阅thisthis Q&A,了解代码实际编译方式的示例。 请记住,CPU 运行 asm,而不是直接运行 C。
不过,这个例子太小太简单,实际上不能用作基准。见Idiomatic way of performance evaluation?


对于隐式长度字符串,您的第二种方式等效于 strcpy。这比较慢,因为它必须搜索终止的 0 字节,如果在内联和展开循环后编译时不知道它。

特别是如果您像这样手动为非常量字符串执行此操作;现代 gcc/clang 无法自动矢量化循环,程序无法在第一次迭代之前计算行程计数。即它们在 strlen 和 strcpy 等搜索循环中失败。

如果您实际上只是调用strcpy(dst, src),编译器将以某种有效的方式内联扩展它,或者发出对库函数的实际调用。 libc 函数使用手写 asm 来高效地执行此操作,尤其是在 SIMD 可以提供帮助的 x86 等 ISA 上。例如对于 x86-64,glibc 的 AVX2 版本 (https://code.woboq.org/userspace/glibc/sysdeps/x86_64/multiarch/strcpy-avx2.S.html) 应该能够在 Zen2 和 Skylake 等主流 CPU 上为中等大小的副本在每个时钟周期复制 32 个字节,其中源和目标处于高速缓存中。

现代 GCC/clang 似乎 将这种模式识别为 strcpy 他们识别 memcpy 等效循环的方式,所以如果你想有效地复制未知大小的 C 字符串,你需要使用实际strcpy。 (或者更好,stpcpy to get a pointer to the end,所以你知道字符串的长度之后,允许你使用显式长度的东西而不是下一个函数也必须扫描字符串的长度。)

一次使用一个char 自己编写它最终会使用字节加载/存储指令,因此每个时钟周期最多可以传输 1 个字节。 (或者在 Ice Lake 上接近 2,可能在 5 宽前端用于加载/宏融合测试/jz/存储的瓶颈。)因此,对于具有运行时变量源的中型到大型副本来说,这是一场灾难,其中编译器无法删除循环。

(https://agner.org/optimize/ 用于 x86 CPU 的性能。其他架构大致相似,除了 SIMD 对 strcpy 有多么有用。没有 x86 的高效 SIMD->integer 对 SIMD 比较结果进行分支能力的 ISA 可能需要使用通用Why does glibc's strlen need to be so complicated to run quickly? 中的整数 bithacks - 但请注意,这是 glibc 的可移植 C 回退,仅在没有人编写手动调整的 asm 的少数平台上使用。)

@0___________ claims 对于 1024 个字符的字符串,他们展开的 char-at-a-time 循环比 glibc strcpy 快,但这是不合理的,可能是基准测试方法错误的结果。 (比如编译器优化无法通过基准测试,或者 libc strcpy 的页面错误开销或惰性动态链接。)


相关问答:

【讨论】:

  • @dn07a 你应该查找 memmove stackoverflow.com/questions/28623895/…
  • @PeterCordes 我不是这个主题的专家,但我知道 memcpy 不是最快的方法,特别是如果你多次调用它memmove() 类似于memcpy() 因为它也将数据从源复制到目标。当源地址和目标地址重叠时,memcpy() 会导致问题,因为memcpy() 只是将数据从一个位置一个接一个地复制到另一个位置。
  • 当复制相同字节数时,memcpy() 将比 strcpy() 更快。唯一一次 strcpy() 或其任何“安全”等价物会胜过 memcpy() 是当字符串的最大允许大小远大于其实际大小时。
  • @dinolin:如果memcpy 不是在非重叠缓冲区之间复制 N 个字节的最快方法,那么您的 C 实现存在性能错误。 (或者您发现您的 libc memcpy 被调整为支持您正在使用的案例之外的案例。例如,花费大量时间分支以最佳地处理非常大和/或未对齐的副本,但是您仅将它用于小的对齐副本,因此更简单的东西很好,并且在实际复制之前做的工作更少)。
  • @dinolin:当您谈论 strcpy 击败 memcpy 以获得大缓冲区时,您谈论的是对 memcpy(dst, src, 1024) 的天真/蛮力使用,它总是复制所有说 1024 char src[1024] 输入缓冲区的字节数,而不是直到终止零?是的,当然,如果您要进行不同数量的复制。但这很愚蠢;如果您使用像 ssize_t size = read(fd, buf, bufsiz); 这样的函数,您将使用 memcpy,因此您已经在变量中拥有实际大小。 (虽然是的,对于像 32 或 64 字节这样的小尺寸,蛮力复制一切实际上都很好。)
【解决方案3】:

一般来说,复制字符串最有效的方法是手动展开循环以尽量减少所需的操作次数。

例子:

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 实现使用了一种非常相似的方法。

【讨论】:

  • 这个答案似乎不值得重新提出问题并丢弃我找到的重复项列表 (stackoverflow.com/posts/70793972/revisions);该问题并未指定避免现有的库函数(编译器将其视为内置函数),因此您可以在纯 C 中手动执行的任何操作在大多数目标上都会变得更糟。充其量是相等的,或者在某些无法一次复制超过一个字节的慢速平台上,如果错过优化错误可能会更好。
  • @PeterCordes memcpy(a,b,strlen(b)) 肯定不会有效率。 2.查看程序集实现 - 然后发表评论。 3. 当字符串的 len 已知编译时间等时,大多数欺骗都是无用的。 4. strlen 是一个完全不同的故事 - 我们讨论 strcpy。 5. 首先证明并检查您的索赔,然后是 DV。
  • @PeterCordes 你避开这个话题。我从未说过它会比strcpy 更快。问题是如何在 C 语言中最有效地拥有实现strcpy。你报复 DV 重新打开你的关闭。
  • 这不是问题所说的,也不是它的意图。注意the OP's comment 好吧,所以基本上 memcpy() 是要走的路。它们看起来非常适合标准库函数,可以利用特定于该平台的硬件功能。
  • 对于什么测试用例,在什么系统上? 也没有证明什么。如果您只是使用惰性动态链接测试对strcpy 的单个调用,那么您将把它作为strcpy 成本的一部分来衡量。 IDK 您可能遇到了哪些其他基准测试方法问题,或者您只是在使用非常短的字符串进行测试。
猜你喜欢
  • 1970-01-01
  • 2017-01-21
  • 1970-01-01
  • 2014-03-01
  • 2010-10-10
  • 2020-12-01
  • 2010-11-21
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多