为啥 GCC 会在我的机器上创建额外的汇编指令?

Posted

技术标签:

【中文标题】为啥 GCC 会在我的机器上创建额外的汇编指令?【英文标题】:Why does GCC create extra assembly instructions on my machine?为什么 GCC 会在我的机器上创建额外的汇编指令? 【发布时间】:2019-10-05 12:40:03 【问题描述】:

我开始使用 SSE/AVX 内部函数已经有一段时间了。我最近开始为矩阵转置编写标题。我使用了很多if constexpr 分支,以便编译器始终根据一些模板参数选择最佳指令集。现在我想通过objdump查看本地反汇编来检查一切是否按预期工作。使用 Clang 时,我得到一个清晰的输出,它基本上只包含与使用的内在函数相对应的汇编指令。但是,如果我使用 GCC,反汇编会非常臃肿,并带有额外的指令。对 Godbolt 的快速检查表明,GCC 反汇编中的那些额外指令不应该存在。

这是一个小例子:

#include <x86intrin.h>
#include <array>

std::array<__m256, 1> Test(std::array<__m256, 1> a)

    std::array<__m256, 1> b;

    b[0] = _mm256_unpacklo_ps(a[0], a[0]);
    return b;

我用-march=native -Wall -Wextra -Wpedantic -pthread -O3 -DNDEBUG -std=gnu++1z 编译。然后我在目标文件上使用objdump -S -Mintel libassembly.a &gt; libassembly.dump。对于 Clang (6.0.0),结果是:

In archive libassembly.a:

libAssembly.cpp.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <_Z4TestSt5arrayIDv8_fLm1EE>:
   0:   c4 e3 7d 04 c0 50       vpermilps ymm0,ymm0,0x50
   6:   c3                      ret    

与Godbolt一样返回:Godbolt - Clang 6.0.0

对于 GCC (7.4),输出为

In archive libassembly.a:

libAssembly.cpp.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <_Z4TestSt5arrayIDv8_fLm1EE>:
   0:   4c 8d 54 24 08          lea    r10,[rsp+0x8]
   5:   48 83 e4 e0             and    rsp,0xffffffffffffffe0
   9:   c5 fc 14 c0             vunpcklps ymm0,ymm0,ymm0
   d:   41 ff 72 f8             push   QWORD PTR [r10-0x8]
  11:   55                      push   rbp
  12:   48 89 e5                mov    rbp,rsp
  15:   41 52                   push   r10
  17:   48 83 ec 28             sub    rsp,0x28
  1b:   64 48 8b 04 25 28 00    mov    rax,QWORD PTR fs:0x28
  22:   00 00 
  24:   48 89 45 e8             mov    QWORD PTR [rbp-0x18],rax
  28:   31 c0                   xor    eax,eax
  2a:   48 8b 45 e8             mov    rax,QWORD PTR [rbp-0x18]
  2e:   64 48 33 04 25 28 00    xor    rax,QWORD PTR fs:0x28
  35:   00 00 
  37:   75 0c                   jne    45 <_Z4TestSt5arrayIDv8_fLm1EE+0x45>
  39:   48 83 c4 28             add    rsp,0x28
  3d:   41 5a                   pop    r10
  3f:   5d                      pop    rbp
  40:   49 8d 62 f8             lea    rsp,[r10-0x8]
  44:   c3                      ret    
  45:   c5 f8 77                vzeroupper 
  48:   e8 00 00 00 00          call   4d <_Z4TestSt5arrayIDv8_fLm1EE+0x4d>

如您所见,还有很多附加说明。与此相反,Godbolt 不包含所有这些额外的指令:Godbolt - GCC 7.4

那么这里发生了什么?我刚刚开始学习汇编,所以对于有汇编经验的人来说可能完全清楚,但我有点困惑为什么 GCC 在我的机器上创建这些额外的指令。

提前问候并感谢您。

编辑

为避免进一步混淆,我只是使用以下代码编译:

gcc-7 -I/usr/local/include -O3 -march=native -Wall -Wextra -Wpedantic -pthread -std=gnu++1z -o test.o -c /&lt;PathToFolder&gt;/libAssembly.cpp

输出保持不变。我不确定这是否相关,但它会生成警告: warning: ignoring attributes on template argument ‘__m256 aka __vector(8) float’ [-Wignored-attributes]

通常我会忽略这个警告,这应该不是问题:

Implication of GCC warning: ignoring attributes on template argument (-Wignored-attributes)

处理器是Intel(R) Core(TM) i7-6700K CPU @ 4.00GHz

这里是gcc -v

gcc-7 -v
Using built-in specs.
COLLECT_GCC=gcc-7
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/7/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 7.4.0-1ubuntu1~18.04.1' --with-bugurl=file:///usr/share/doc/gcc-7/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++ --prefix=/usr --with-gcc-major-version-only --program-suffix=-7 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 7.4.0 (Ubuntu 7.4.0-1ubuntu1~18.04.1) 

【问题讨论】:

我无法使用本地 GCC 安装重现它。 why GCC - 等等,哪个 gcc?您发布了一个指向 gcc 7.4 的 Godbolt 链接,该链接生成 vunpcklps ymm0, ymm0, ymm0。那么你呈现的输出是什么?适合你的机器吗?你用-march=native,你的本地机器支持SSE/AVX吗? 在“坏”的例子中,你确定你编译时启用了优化吗?您在问题中提到了-O3,但在我看来,这就像未优化的输出。 @JasonR:很确定。我正在使用 CMake,详细输出告诉我该文件是使用 -march=native -Wall -Wextra -Wpedantic -pthread -O3 -DNDEBUG -std=gnu++1z -o CMakeFiles/assembly.dir/libAssembly.cpp.o 构建的 @KamilCuk 输出是在我的机器上使用 gcc 7.4 生成的,我的机器支持 AVX2 指令。 【参考方案1】:

使用-fno-stack-protector


您的本地 GCC 默认为 -fstack-protector-strong,但 Godbolt 的 GCC 安装没有。

mov rax,QWORD PTR fs:0x28 是线索fs:40 aka fs:0x28 的线程本地存储是 GCC 保持其堆栈 cookie 不变的地方。在ret 之后的callcall __stack_chk_fail(但是您在没有使用objdump -dr 来显示重定位的情况下反汇编了.o,因此占位符+0 偏移看起来仍然是此函数中的目标)。

由于您有数组(或包含数组的类),因此即使它们的大小是编译时常量,stack-protector-strong 也会发挥作用。所以你得到了存储堆栈cookie的代码,然后检查它并在堆栈溢出时分支。 (即使是这个 MVCE 中大小为 1 的数组也足以触发它。)

在堆栈上以 32 字节对齐(对于 __m256)创建数组需要 32 字节对齐,并且您的 GCC 比 GCC8 更旧,因此您会获得构建堆栈的完整副本的可笑笨拙的堆栈对齐代码包含返回地址的框架。 Generated assembly for extended alignment of stack variables(需要说明的是,GCC8 仍然在这里对齐堆栈,只是在上面浪费了更少的指令。)

这几乎是一个错过的优化; gcc 从未真正溢出或重新加载这些数组,因此它可以将它们连同堆栈对齐一起优化掉,就像没有堆栈保护器一样。

在更多情况下,最近的 GCC 在为对齐的局部变量优化内存之后,更擅长优化堆栈对齐,但这一直是 AVX 代码中持续错过的优化。幸运的是,循环函数的成本可以忽略不计。只要小助手函数内联。


Compiling on Godbolt 和 -fstack-protector-strong 会重现您的输出。 较新的 GCC,包括当前的trunk pre-10,仍然缺少优化,但堆栈对齐花费更少的指令,因为它只使用 RBP 作为帧指针并对齐 RSP,然后引用相对于对齐 RSP 的局部变量。它仍然检查堆栈 cookie(在存储和检查之间没有说明)。

在您的桌面上,使用 -fno-stack-protector 编译应该可以生成良好的 asm。

【讨论】:

非常感谢。有些人在这里拥有多少知识,真是太棒了:)。使用-fno-stack-protector 编译确实可以解决问题。但是,我使用 GCC 8.3 对 ubuntu 19.04 进行了分发升级。当我在没有标志的情况下编译时问题仍然存在。它不应该在 GCC 8 + 中消失吗? --- 1 的数组大小实际上是我使用的函数模板的边缘情况,我认为编译器无论如何都会优化它。 @wychmaster:哎呀,你是对的。我没有仔细看,错过了即使使用 GCC9 堆栈对齐仍然存在。 asm 输出更短,但这是因为 GCC8 在没有 VLA / alloca 的函数中具有更简单的堆栈对齐,而不是因为堆栈对齐被完全优化掉了。愚蠢的我,修正了我的答案。 @wychmaster:内联后它只会影响调用者一次,而不是每次调用内联函数时影响一次。或者如果调用者已经需要堆栈对齐,那么就没有额外的成本。但是是的,它可以在 x86-64 System V 调用约定中进行优化。包含一个 __m256[1] 成员的 class 在向量寄存器中传递和返回,这与在 Windows x64 向量调用中不同,在类中将强制通过引用传递/返回。 godbolt.org/z/piojOa(在内联时仍会优化,但独立版本会保证开销。) 再次感谢您的解释。我只需要干净的程序集来检查我的矩阵转置函数是否按预期工作,并查看是否实际应用了我期望的所有优化。除此之外,只要对我的基准测试没有重大影响,我就可以忍受它。我很好奇 MSVC 开销的影响有多大。我想我也必须最终开始在 Windows 中测试我的代码。但是,我总是可以使用 MinGW 作为 MSVC 的替代品:p @wychmaster:MinGW GCC(仍然是 AFAIK)不能与 AVX 一起使用,因为它在溢出 __m256 locals 或类似的东西之前没有对齐堆栈。使用 clang(总的来说,它比 MSVC 优化得更好,并且实现了 GNU C/C++ 扩展)。但无论如何,它们在面向 Windows 时仍将使用 Windows x64 调用约定。不过,只要您的函数内联,就没有实际的传递/返回开销。

以上是关于为啥 GCC 会在我的机器上创建额外的汇编指令?的主要内容,如果未能解决你的问题,请参考以下文章

编译系统

汇编指令对应的机器码 ,问 为啥这个汇编指令对应的是这个机器码?

gcc基本功能以及常见编译选项

用Vcomputer机器指令与汇编指令分别编程

汇编语言第一章总结

为啥 TensorFlow 会在 TensorBoard 可视化中为我的变量创建额外的命名空间?