【问题标题】:Why does this code snippet produce radically different assembly code in C and C++?为什么这段代码片段会在 C 和 C++ 中产生截然不同的汇编代码?
【发布时间】:2018-07-27 23:21:50
【问题描述】:
struct v {
    int val[16];
};

struct v test(struct v a, struct v b) {
    struct v res;

    for (int i = 0; i < 16; i++)
        res.val[i] = a.val[i] + b.val[i];

    return res;
}

编译为 C++,GCC 7.2 发出:

  push r10
  vmovdqu32 zmm0, ZMMWORD PTR [rsp+16]
  mov rax, rdi
  vpaddd zmm0, zmm0, ZMMWORD PTR [rsp+80]
  lea r10, [rsp+16]
  vmovdqu32 ZMMWORD PTR [rdi], zmm0
  pop r10

编译为 C:

  lea r10, [rsp+8]
  and rsp, -64
  mov rax, rdi
  push QWORD PTR [r10-8]
  push rbp
  mov rbp, rsp
  push r10
  vmovdqu32 zmm0, ZMMWORD PTR [r10]
  vpaddd zmm0, zmm0, ZMMWORD PTR [r10+64]
  vmovdqa64 ZMMWORD PTR [rbp-112], zmm0
  vmovdqa64 xmm0, XMMWORD PTR [rbp-112]
  vmovups XMMWORD PTR [rdi], xmm0
  vmovdqa64 xmm0, XMMWORD PTR [rbp-96]
  vmovups XMMWORD PTR [rdi+16], xmm0
  vmovdqa64 xmm0, XMMWORD PTR [rbp-80]
  vmovups XMMWORD PTR [rdi+32], xmm0
  vmovdqa64 xmm0, XMMWORD PTR [rbp-64]
  vmovups XMMWORD PTR [rdi+48], xmm0
  pop r10
  pop rbp
  lea rsp, [r10-8]

在 Godbolt 编译器资源管理器上编译为 C 与 C++:

clang x86 asm:https://godbolt.org/g/FfrKTf
gcc x86 asm:https://godbolt.org/g/SZQqqt

相同的代码 sn-p 在 gcc 和 clang 中为 C 和 C++ 生成显着不同的代码:

  • 循环的自动向量化方式不同:gcc 使用未对齐的加载/存储,但 g++ 使用标量直到对齐边界,-march=sandybridge 或任何比 AVX2 更窄的东西。

    这不是 x86 特定的:AArch64 SIMD 的自动矢量化也会发生同样的事情。

  • C 版本实际上将结果存储到堆栈上的本地 res,然后使用 SIMD 副本从那里复制到返回值指针(有时副本的向量宽度与刚刚存储数据的循环的向量宽度不同)。

    在禁用自动矢量化 (-fno-tree-vectorize) 后,gcc 仍使用 SIMD 加载/存储来复制结果。

有人知道为什么会这样吗?

是否有一些 C++ 标准中的对齐保证,但 C 标准中没有?这是 ABI 问题吗?还是编译器资源管理器上有一些奇怪的编译选项?

【问题讨论】:

  • 能否复制粘贴问题中的代码和汇编程序?
  • 因为 C++ 有复制省略?试试-fno-elide-constructors
  • C 前端与 C++ 前端的 gcc 和 clang 看起来像是错过了优化。它们必须将程序的不同内部表示形式提供给中间端,因此 C 版本会额外复制到堆栈上的本地。 ICC 使用 / 不使用 -xc 进行相同的 asm。
  • 我认为最终这将只是“因为它们是不同的编译器,它们只是在某些方面有所不同,[编译器]代码专注于某些方法而可能以其他方法为代价”
  • @user902384 :您一定是新来的,但 Godbolt 是一个备受推崇的领域,并且被问答作者大量使用,尤其是在 C/C++/Assembly 标签中。

标签: c++ c gcc assembly simd


【解决方案1】:

根据编译器转储 C Gimple IR 在堆栈上工作中间对象(然后将其复制到返回值)

$ gcc ../tmp.c -fdump-tree-gimple -O2 -S -std=c99
$ cat tmp.c.004t.gimple
...
    <D.1621>:
    D.1626 = a.val[i];
    D.1627 = b.val[i];
    D.1628 = D.1626 + D.1627;
    res.val[i] = D.1628;
    i = i + 1;
    <D.1622>:
    if (i <= 15) goto <D.1621>; else goto <D.1623>;
    <D.1623>:
  }
  <retval> = res;
  return <retval>;

而 C++ 直接在 retval temp 上运行:

$ g++ ../tmp.c -fdump-tree-gimple -O2 -S
$ cat tmp.c.004t.gimple
    <D.2250>:
    D.2254 = a.val[i];
    D.2255 = b.val[i];
    D.2256 = D.2254 + D.2255;
    <retval>.val[i] = D.2256;
    i = i + 1;
    <D.2251>:
    if (i <= 15) goto <D.2250>; else goto <D.2248>;
    <D.2248>:
  }
  return <retval>;

AFAIR C 和 C++ Gimple 生成器不共享代码,因此会出现这样的异常情况。我鼓励您在GCC Bugzilla 报告此问题,因为这可能是一个常见的性能问题。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-07-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-02-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多