为什么 GCC 在我的机器上创建额外的汇编指令?
Why does GCC create extra assembly instructions on my machine?
自从我开始使用 SSE/AVX 内部函数以来已经有一段时间了。我最近开始写一个 header 用于矩阵转置。我使用了很多 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
编译。然后我在 object 文件上使用 objdump -S -Mintel libassembly.a > 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
与神马一样returns: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 /<PathToFolder>/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)
使用-fno-stack-protector
您的本地 GCC 默认为 -fstack-protector-strong
但 Godbolt 的 GCC 安装没有。
mov rax,QWORD PTR fs:0x28
是线索; fs:40
又名 fs:0x28
的线程本地存储是 GCC 保持其堆栈 cookie 不变的地方。 ret
之后的 call
是 call __stack_chk_fail
(但是你反汇编了一个 .o
而没有使用 objdump -dr
来显示重定位,所以占位符 +0
偏移只是看起来仍然是这个函数中的一个目标)。
由于您有数组(或包含数组的 class),即使它们的大小是编译时常量,stack-protector-strong 也会起作用。所以你得到了存储堆栈 cookie 的代码,然后检查它并在堆栈溢出时分支。 (即使这个 MVCE 中大小为 1 的数组也足以触发它。)
以 32 字节对齐(对于 __m256
)在堆栈上创建数组需要 32 字节对齐,并且您的 GCC 比 GCC8 更旧,因此您得到的是可笑的笨拙堆栈对齐代码,用于构建完整的堆栈帧的副本,包括 return 地址。 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。
自从我开始使用 SSE/AVX 内部函数以来已经有一段时间了。我最近开始写一个 header 用于矩阵转置。我使用了很多 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
编译。然后我在 object 文件上使用 objdump -S -Mintel libassembly.a > 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
与神马一样returns: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 /<PathToFolder>/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)
使用-fno-stack-protector
您的本地 GCC 默认为 -fstack-protector-strong
但 Godbolt 的 GCC 安装没有。
mov rax,QWORD PTR fs:0x28
是线索; fs:40
又名 fs:0x28
的线程本地存储是 GCC 保持其堆栈 cookie 不变的地方。 ret
之后的 call
是 call __stack_chk_fail
(但是你反汇编了一个 .o
而没有使用 objdump -dr
来显示重定位,所以占位符 +0
偏移只是看起来仍然是这个函数中的一个目标)。
由于您有数组(或包含数组的 class),即使它们的大小是编译时常量,stack-protector-strong 也会起作用。所以你得到了存储堆栈 cookie 的代码,然后检查它并在堆栈溢出时分支。 (即使这个 MVCE 中大小为 1 的数组也足以触发它。)
以 32 字节对齐(对于 __m256
)在堆栈上创建数组需要 32 字节对齐,并且您的 GCC 比 GCC8 更旧,因此您得到的是可笑的笨拙堆栈对齐代码,用于构建完整的堆栈帧的副本,包括 return 地址。 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。