【问题标题】:C string using index or pointer to copy characters使用索引或指针复制字符的 C 字符串
【发布时间】:2019-08-02 11:29:07
【问题描述】:

我有 2 个 (strcpy) 函数的源代码,我想知道哪一个更快,性能更高...

unsigned
strcpy(const char * str, char * des) {
    register const char * ptr = str;

    while ((*des = *str)) {
        str++;
        des++;
    }

    return (str - ptr);
}

unsigned
strcpy2(const char * str, char * des) {
    register unsigned i = 0;

    while ((des[i] = str[i])) i++;

    return i;
}

第一个使用 str 和 des 地址,第二个使用 index ... 第一个有一个额外的(++),所以乍一看,第一个函数的性能低于第二个,因为对每个字符都做了额外的(++)但是当我在 GCC 中使用(-O3)优化时,结果(汇编代码)告诉我别的东西(第一个strcpy性能更高,动作更少)

strcpy:
        movzbl  (%rdi), %eax
        movb    %al, (%rsi)
        testb   %al, %al
        je      .L4
        movq    %rdi, %rax
.L3:
        movzbl  1(%rax), %edx
        addq    $1, %rax
        addq    $1, %rsi
        movb    %dl, (%rsi)
        testb   %dl, %dl
        jne     .L3
        subl    %edi, %eax
        ret
.L4:
        xorl    %eax, %eax
        ret
strcpy2:
        movzbl  (%rdi), %eax
        testb   %al, %al
        movb    %al, (%rsi)
        movl    $0, %eax
        je      .L10
.L9:
        leal    1(%rax), %ecx
        movzbl  (%rdi,%rcx), %edx
        movq    %rcx, %rax
        movb    %dl, (%rsi,%rcx)
        testb   %dl, %dl
        jne     .L9
        ret
.L10:
        ret

是真的吗?第一个 strcpy 具有更高的性能(性能 = 更少的操作和更快)?

【问题讨论】:

  • 两者都附加了一个多余的终止零。
  • 我的意思是循环之后的赋值是多余的。您可以删除它。
  • @Jason,人类在从源代码估计程序性能方面非常糟糕(汇编也是源代码)。此外,相对性能可能会因程序运行的硬件而异。测试是最好、最可靠的方法。
  • 第二版 asm 有严重的优化缺失(与不自动矢量化不同,因为 gcc 从不为搜索循环这样做)。 lea+mov 显然比 inc 更糟糕,这将使其在除 Sandybridge/Ivybridge 之外的 CPU 上降至 4 个融合域 uop,其中存储的索引寻址模式将取消分层。 Micro fusion and addressing modes显然,这些都很糟糕,一次只能复制 1 个字节,甚至无法在 Intel CPU 上管理每个时钟周期 1 个字节。 SSE2 是 x86-64 的基准。
  • @JohnBollinger:为自己说话;我很确定 Intel Haswell 和更高版本将以每字节 1.25 个周期为大型副本运行这些循环中的任何一个,在前端遇到瓶颈。对于具有页面错误和缓存未命中的大型副本,它会减慢一小部分速度,但对于硬件预取来说,它的速度足以在大部分时间轻松跟上。

标签: c performance gcc x86-64 micro-optimization


【解决方案1】:

没关系。 两者都可能比标准库中的 strcpy 慢。 如果你不自己实现 strcpy,GCC 会做很好的内联代码。

现代 CPU 的性能瓶颈是缓存和 RAM 带宽,而不是操作码。

【讨论】:

  • 不,@Jason,他们不是在开玩笑。
  • 我不敢相信你这么说!!!!!!!!!甚至使用函数 num 1 的 GCC !!!! github.com/gcc-mirror/gcc/blob/master/libgcc/memcpy.c ....(notan 所说的对这个功能没有用...它是用于大功能)
  • @Jason:这是通用的后备实现,或者供 gcc 内部使用。这不是 gcc 内联的。为此,请查看启用了优化的编译器 output。 (如-O3 -march=native)。对于编译时常量大小,它将内联 movupsmovdqu 指令,或者与 AVX 一起使用 vmovdqu 和 32 字节 YMM 向量寄存器。
  • 考虑:GCC 的实现者已经致力于实现strcpy()(包括内在函数和优化器支持)几十年。为什么你会期望你的代码会胜过他们的?
  • @Jason:如果他们在实现提供优化实现时实际使用它们,那么是的,我是。您只希望在缺少 libc 函数的情况下将其作为后备。或者,如果您的字符串几乎总是很小(例如 4 个字节或更少),因此函数调用的成本高于运行缓慢的逐字节循环的成本。另请参阅Why is this code 6.5x slower with optimizations enabled?,其中 gcc 内联慢速 strlen 对于很长的字符串确实很糟糕,以便详细分析 asm 实现。
【解决方案2】:

第二段摘录无效用于复制字符串。您在指针为 64 位的平台上使用 32 位 unsigned int。因此,编译器需要非常小心来考虑索引中的 32 位模运算。如果您将i 更改为size_t应该那么编译器可以消除零扩展和LEAs。

无论如何,您的版本总是比编译器版本慢 - 因为在编译器中 strcpy 是一个内在函数 - 编译器可以知道将其替换为 @987654328 @ 甚至只是将单个字符加载到寄存器中,而不是盲目地将一些字节从内存的一部分复制到另一部分。

【讨论】:

  • 它是零扩展,不是符号扩展,因此只需编写 32 位寄存器即可。 LEA+MOV 而不是 ADD 是因为编译器想要返回哪个值。 (GCC 很容易被愚弄,在循环中放置额外的指令来设置它在循环之后想要寄存器的方式,而不是仅仅在循环之后放置 mov。)
  • 哎呀,将 zign 修复为 sero 扩展。
  • 有趣,事实证明你是对的,size_t 确实让 gcc 将 LEA/MOV 优化为 ADD。 godbolt.org/z/ptwpcM。 clang 通过使用1(%rsi,%rax) 寻址模式并从rax=-1 开始,大大简化了函数。副本处于循环状态,因此始终至少运行一次;只有增量可能需要运行零次,但它会对此进行调整。
  • @PeterCordes 最奇怪的是 signed int 在 GCC 上看起来并不比 unsigned 好多少,编译器“知道”它需要返回一个 32 位值...
  • 哦,有趣。是的,循环之后额外的movl %eax, %r8d inside 而不仅仅是movl %eax, %eax,正是我所说的那种GCC愚蠢。至少它不在关键路径上,因此它不依赖于 mov-elimination 来避免循环计数器 dep 链上 2 个周期/迭代的瓶颈。 (我正在查看 gcc9.1,它使用 add $1, %raxmovr8d 中,这只能在 outside 循环中读取。godbolt.org/z/e2tIrm GCC8.3 类似)跨度>
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-11-01
  • 1970-01-01
  • 1970-01-01
  • 2013-09-27
  • 1970-01-01
  • 1970-01-01
  • 2020-10-30
相关资源
最近更新 更多