为什么 gcc 不将 _mm256_loadu_pd 解析为单个 vmovupd?
Why doesn't gcc resolve _mm256_loadu_pd as single vmovupd?
我正在编写一些 AVX 代码,我需要从可能未对齐的内存中加载。我目前正在加载 4 doubles,因此我会使用内部指令 _mm256_loadu_pd;我写的代码是:
__m256d d1 = _mm256_loadu_pd(vInOut + i*4);
然后我使用选项 -O3 -mavx -g
进行编译,随后使用 objdump 来获取汇编代码以及带注释的代码和行 (objdump -S -M intel -l avx.obj
)。
当我查看底层汇编代码时,我发现以下内容:
vmovupd xmm0,XMMWORD PTR [rsi+rax*1]
vinsertf128 ymm0,ymm0,XMMWORD PTR [rsi+rax*1+0x10],0x1
我期待看到这个:
vmovupd ymm0,XMMWORD PTR [rsi+rax*1]
并完全使用 256 位寄存器 (ymm0),而不是 gcc 已决定填充 128 位部分(xmm0) 然后用 vinsertf128.
再次加载另一半
有人能解释一下吗?
在 MSVC VS 2012 中使用单个 vmovupd 编译等效代码。
我 运行 gcc (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0
Ubuntu 18.04 x86-64.
GCC 的通用调整 splits unaligned 256-bit loads 以帮助较旧的处理器。 (我相信,随后的更改避免了在通用调整中拆分负载。)
您可以使用 -mtune=intel
或 -mtune=skylake
之类的东西针对更新的 Intel CPU 进行调整,您将按预期获得一条指令。
GCC 的默认调优 (-mtune=generic
) 包括 -mavx256-split-unaligned-load
和 -mavx256-split-unaligned-store
,因为这对某些 CPUs(例如第一代 Sandybridge 和一些 AMD CPUs)在某些情况下当内存实际上在 运行 时未对齐。
如果你不想要这个,使用-O3 -mno-avx256-split-unaligned-load -mno-avx256-split-unaligned-store
,或者更好,使用-mtune=haswell
。或者使用-march=native
来优化你的自己的电脑。没有 "generic-avx2" 调整。 (https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html).
Intel Sandybridge 运行s 256 位加载作为单个 uop,在加载端口中需要 2 个周期。 (与 AMD 不同,AMD 将所有 256 位向量指令解码为 2 个单独的 uops。)Sandybridge 存在未对齐的 256 位加载问题(如果地址实际上在 运行 时间未对齐)。我不知道细节,也没有找到关于减速到底是什么的具体信息。也许是因为它使用了带 16 字节组的组缓存?但是 IvyBridge 可以更好地处理 256 位负载,并且仍然有银行缓存。
根据关于实现该选项 (https://gcc.gnu.org/ml/gcc-patches/2011-03/msg01847.html) 的代码的 GCC 邮件列表消息,“它加快了某些 SPEC CPU 2006 基准测试高达 6% ."(我认为这是针对 Sandybridge 的,当时唯一存在的英特尔 AVX CPU。)
但是如果内存在 运行 时实际上是 32 字节对齐的,即使在 Sandybridge 和大多数 AMD CPUs1。所以使用这个调整选项,你可能会因为没有告诉你的编译器关于对齐保证而失败。如果你的循环 运行s 在对齐内存 most 的时间,你最好至少用 -mno-avx256-split-unaligned-load
或调整选项编译那个编译单元,这意味着.
在软件中拆分始终会增加成本。让硬件处理它可以使对齐的情况非常有效(Piledriver1 上的存储除外),而未对齐的情况可能比某些 CPU 上的软件拆分慢。所以这是悲观的方法,如果数据真的很可能在 运行 时间未对齐,而不是仅仅不保证在编译时始终对齐,那么它是有意义的。例如也许您有一个大部分时间都使用对齐缓冲区调用的函数,但您仍然希望它在使用未对齐缓冲区调用的罕见/小情况下工作。在这种情况下,即使在 Sandybridge 上,split-load/store 策略也不合适。
缓冲区通常为 16 字节对齐但不是 32 字节对齐,因为 x86-64 glibc 上的 malloc
(以及 libstdc++ 中的 new
)returns 16 字节对齐缓冲区(因为 alignof(maxalign_t) == 16
)。对于大缓冲区,指针通常位于页面开始后的 16 个字节,因此对于大于 16 的对齐,它总是未对齐。请改用 aligned_alloc
。
请注意 -mavx
和 -mavx2
根本不会更改 调整 选项: gcc -O3 -mavx2
仍然调整 所有 CPUs,包括那些实际上不能 运行 AVX2 指令。这非常愚蠢,因为如果针对 "the average AVX2 CPU" 进行调整,您应该使用单个未对齐的 256 位加载。不幸的是,gcc 没有选择这样做,并且 -mavx2
并不意味着 -mno-avx256-split-unaligned-load
或任何东西。 请参阅 https://gcc.gnu.org/bugzilla/show_bug.cgi?id=80568 and https://gcc.gnu.org/bugzilla/show_bug.cgi?id=78762 以了解指令集选择影响调整的功能请求。
这就是为什么您应该使用 -march=native
制作供本地使用的二进制文件,或者 -march=sandybridge -mtune=haswell
制作可以 运行 在广泛的机器上使用的二进制文件,但可能大多数 运行 在具有 AVX 的较新硬件上。 (请注意,即使是 Skylake Pentium/Celeron CPUs 也没有 AVX 或 BMI2;可能在 CPUs 上 256 位执行单元或寄存器文件的上半部分有任何缺陷,它们禁用 VEX 前缀的解码并将它们作为低端奔腾出售。)
gcc8.2的调优选项如下。 (-march=x
表示 -mtune=x
)。 https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html.
我通过使用 -O3 -fverbose-asm
编译并查看包含所有隐含选项的完整转储的注释来检查 on the Godbolt compiler explorer。我包含了 _mm256_loadu/storeu_ps
函数,以及一个可以自动矢量化的简单浮点循环,因此我们还可以查看编译器的功能。
使用 -mprefer-vector-width=256
(gcc8) 或 -mno-prefer-avx128
(gcc7 及更早版本)覆盖 -mtune=bdver3
之类的调整选项,并根据需要获得 256 位自动矢量化,而不仅仅是手动矢量化。
- 默认/
-mtune=generic
:-mavx256-split-unaligned-load
和-store
。随着 Intel Haswell 和后来变得越来越普遍,可以说越来越不合适了,我认为最近的 AMD CPUs 的缺点仍然很小。特别是拆分未对齐的 负载 ,AMD 调整选项不启用。
-march=sandybridge
和 -march=ivybridge
:将两者分开。 (我想我已经读过 IvyBridge 改进了对未对齐的 256 位加载或存储的处理,因此它不太适合数据 可能 在 运行 时对齐的情况。 )
-march=haswell
及更高版本:均未启用拆分选项。
-march=knl
: 拆分选项均未启用。 (Silvermont/Atom 没有 AVX)
-mtune=intel
: 拆分选项均未启用。即使使用 gcc8,使用 -mtune=intel -mavx
的自动矢量化也会选择到达 read/write 目标数组的对齐边界,这与 gcc8 仅使用未对齐的正常策略不同。 (再一次,软件处理总是有成本的另一种情况,而不是让硬件处理异常情况。)
-march=bdver1
(推土机):-mavx256-split-unaligned-store
,但不是负载。
它还设置了 gcc8 等效 gcc7 和更早的 -mprefer-avx128
(自动矢量化将仅使用 128 位 AVX,但当然内部函数仍然可以使用 256 位矢量)。
-march=bdver2
(打桩机),bdver3
(压路机),bdver4
(挖掘机)。和推土机一样。他们使用软件预取自动矢量化 FP a[i] += b[i]
循环和足够的展开以每个缓存行仅预取一次!
-march=znver1
(Zen): -mavx256-split-unaligned-store
但不加载,仍然仅使用 128 位进行自动矢量化,但这次没有软件预取。
-march=btver2
(AMD Fam16h, aka Jaguar):既不启用拆分选项,又像 Bulldozer 系列一样自动矢量化,只有 128 位矢量 + SW 预取。
-march=eden-x4
(通过带有 AVX2 的 Eden):既没有启用分割选项,但是 -march
选项甚至没有启用 -mavx
,以及自动矢量化使用 movlps
/ movhps
8 字节加载,这真的很愚蠢。至少使用 movsd
而不是 movlps
来打破错误的依赖。但是如果启用 -mavx
,它会使用 128 位未对齐加载。这里的行为真的很奇怪/不一致,除非有一些奇怪的前端。
选项(例如,作为 -march=sandybridge 的一部分启用,大概也适用于 Bulldozer 系列(-march=bdver2 是打桩机)。当编译器知道内存已对齐时,这并不能解决问题,尽管如此.
脚注 1:AMD Piledriver 有一个性能错误,这使得 256 位存储吞吐量很糟糕:根据 Agner Fog 的 microarch pdf( https://agner.org/optimize/)。此效果在 Bulldozer 或 Steamroller/Excavator.
中不存在
Agner Fog 说 Bulldozer/Piledriver 上的 256 位 AVX 吞吐量一般(不是 loads/stores 特别是)通常比 128 位 AVX 差,部分原因是它无法解码 2 中的指令-2 uop 模式。 Steamroller 使 256 位接近收支平衡(如果不需要额外的洗牌)。但是寄存器-寄存器 vmovaps ymm
指令仍然只受益于推土机系列低 128 位的移动消除。
但是闭源软件或二进制发行版通常没有在每个目标架构上使用 -march=native
构建的奢侈,因此在制作可以 运行 在任何目标架构上的二进制文件时需要权衡支持 AVX CPU。在某些 CPU 上使用 256 位代码获得大幅加速通常是值得的,只要在其他 CPU 上没有灾难性的缺点。
拆分未对齐的 loads/stores 是为了避免在某些 CPU 上出现大问题。在最近的 CPU 上,它需要额外的 uop 吞吐量和额外的 ALU uops。但至少 vinsertf128 ymm, [mem], 1
不需要 Haswell/Skylake 端口 5 上的洗牌单元:它可以 运行 在任何向量 ALU 端口上。 (而且它没有微型熔断器,所以它需要 2 微秒的前端带宽。)
PS:
大多数代码不是由前沿编译器编译的,因此现在更改 "generic" 调整需要一段时间才能使用使用更新的调整编译的代码。 (当然,大多数代码只用 -O2
或 -O3
编译,而且这个选项无论如何只会影响 AVX code-gen。但不幸的是,很多人使用 -O3 -mavx2
而不是 -O3 -march=native
. 所以他们可能会错过 FMA、BMI1/2、popcnt 和他们 CPU 支持的其他东西。
我正在编写一些 AVX 代码,我需要从可能未对齐的内存中加载。我目前正在加载 4 doubles,因此我会使用内部指令 _mm256_loadu_pd;我写的代码是:
__m256d d1 = _mm256_loadu_pd(vInOut + i*4);
然后我使用选项 -O3 -mavx -g
进行编译,随后使用 objdump 来获取汇编代码以及带注释的代码和行 (objdump -S -M intel -l avx.obj
)。
当我查看底层汇编代码时,我发现以下内容:
vmovupd xmm0,XMMWORD PTR [rsi+rax*1]
vinsertf128 ymm0,ymm0,XMMWORD PTR [rsi+rax*1+0x10],0x1
我期待看到这个:
vmovupd ymm0,XMMWORD PTR [rsi+rax*1]
并完全使用 256 位寄存器 (ymm0),而不是 gcc 已决定填充 128 位部分(xmm0) 然后用 vinsertf128.
再次加载另一半有人能解释一下吗?
在 MSVC VS 2012 中使用单个 vmovupd 编译等效代码。
我 运行 gcc (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0
Ubuntu 18.04 x86-64.
GCC 的通用调整 splits unaligned 256-bit loads 以帮助较旧的处理器。 (我相信,随后的更改避免了在通用调整中拆分负载。)
您可以使用 -mtune=intel
或 -mtune=skylake
之类的东西针对更新的 Intel CPU 进行调整,您将按预期获得一条指令。
GCC 的默认调优 (-mtune=generic
) 包括 -mavx256-split-unaligned-load
和 -mavx256-split-unaligned-store
,因为这对某些 CPUs(例如第一代 Sandybridge 和一些 AMD CPUs)在某些情况下当内存实际上在 运行 时未对齐。
如果你不想要这个,使用-O3 -mno-avx256-split-unaligned-load -mno-avx256-split-unaligned-store
,或者更好,使用-mtune=haswell
。或者使用-march=native
来优化你的自己的电脑。没有 "generic-avx2" 调整。 (https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html).
Intel Sandybridge 运行s 256 位加载作为单个 uop,在加载端口中需要 2 个周期。 (与 AMD 不同,AMD 将所有 256 位向量指令解码为 2 个单独的 uops。)Sandybridge 存在未对齐的 256 位加载问题(如果地址实际上在 运行 时间未对齐)。我不知道细节,也没有找到关于减速到底是什么的具体信息。也许是因为它使用了带 16 字节组的组缓存?但是 IvyBridge 可以更好地处理 256 位负载,并且仍然有银行缓存。
根据关于实现该选项 (https://gcc.gnu.org/ml/gcc-patches/2011-03/msg01847.html) 的代码的 GCC 邮件列表消息,“它加快了某些 SPEC CPU 2006 基准测试高达 6% ."(我认为这是针对 Sandybridge 的,当时唯一存在的英特尔 AVX CPU。)
但是如果内存在 运行 时实际上是 32 字节对齐的,即使在 Sandybridge 和大多数 AMD CPUs1。所以使用这个调整选项,你可能会因为没有告诉你的编译器关于对齐保证而失败。如果你的循环 运行s 在对齐内存 most 的时间,你最好至少用 -mno-avx256-split-unaligned-load
或调整选项编译那个编译单元,这意味着.
在软件中拆分始终会增加成本。让硬件处理它可以使对齐的情况非常有效(Piledriver1 上的存储除外),而未对齐的情况可能比某些 CPU 上的软件拆分慢。所以这是悲观的方法,如果数据真的很可能在 运行 时间未对齐,而不是仅仅不保证在编译时始终对齐,那么它是有意义的。例如也许您有一个大部分时间都使用对齐缓冲区调用的函数,但您仍然希望它在使用未对齐缓冲区调用的罕见/小情况下工作。在这种情况下,即使在 Sandybridge 上,split-load/store 策略也不合适。
缓冲区通常为 16 字节对齐但不是 32 字节对齐,因为 x86-64 glibc 上的 malloc
(以及 libstdc++ 中的 new
)returns 16 字节对齐缓冲区(因为 alignof(maxalign_t) == 16
)。对于大缓冲区,指针通常位于页面开始后的 16 个字节,因此对于大于 16 的对齐,它总是未对齐。请改用 aligned_alloc
。
请注意 -mavx
和 -mavx2
根本不会更改 调整 选项: gcc -O3 -mavx2
仍然调整 所有 CPUs,包括那些实际上不能 运行 AVX2 指令。这非常愚蠢,因为如果针对 "the average AVX2 CPU" 进行调整,您应该使用单个未对齐的 256 位加载。不幸的是,gcc 没有选择这样做,并且 -mavx2
并不意味着 -mno-avx256-split-unaligned-load
或任何东西。 请参阅 https://gcc.gnu.org/bugzilla/show_bug.cgi?id=80568 and https://gcc.gnu.org/bugzilla/show_bug.cgi?id=78762 以了解指令集选择影响调整的功能请求。
这就是为什么您应该使用 -march=native
制作供本地使用的二进制文件,或者 -march=sandybridge -mtune=haswell
制作可以 运行 在广泛的机器上使用的二进制文件,但可能大多数 运行 在具有 AVX 的较新硬件上。 (请注意,即使是 Skylake Pentium/Celeron CPUs 也没有 AVX 或 BMI2;可能在 CPUs 上 256 位执行单元或寄存器文件的上半部分有任何缺陷,它们禁用 VEX 前缀的解码并将它们作为低端奔腾出售。)
gcc8.2的调优选项如下。 (-march=x
表示 -mtune=x
)。 https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html.
我通过使用 -O3 -fverbose-asm
编译并查看包含所有隐含选项的完整转储的注释来检查 on the Godbolt compiler explorer。我包含了 _mm256_loadu/storeu_ps
函数,以及一个可以自动矢量化的简单浮点循环,因此我们还可以查看编译器的功能。
使用 -mprefer-vector-width=256
(gcc8) 或 -mno-prefer-avx128
(gcc7 及更早版本)覆盖 -mtune=bdver3
之类的调整选项,并根据需要获得 256 位自动矢量化,而不仅仅是手动矢量化。
- 默认/
-mtune=generic
:-mavx256-split-unaligned-load
和-store
。随着 Intel Haswell 和后来变得越来越普遍,可以说越来越不合适了,我认为最近的 AMD CPUs 的缺点仍然很小。特别是拆分未对齐的 负载 ,AMD 调整选项不启用。 -march=sandybridge
和-march=ivybridge
:将两者分开。 (我想我已经读过 IvyBridge 改进了对未对齐的 256 位加载或存储的处理,因此它不太适合数据 可能 在 运行 时对齐的情况。 )-march=haswell
及更高版本:均未启用拆分选项。-march=knl
: 拆分选项均未启用。 (Silvermont/Atom 没有 AVX)-mtune=intel
: 拆分选项均未启用。即使使用 gcc8,使用-mtune=intel -mavx
的自动矢量化也会选择到达 read/write 目标数组的对齐边界,这与 gcc8 仅使用未对齐的正常策略不同。 (再一次,软件处理总是有成本的另一种情况,而不是让硬件处理异常情况。)
-march=bdver1
(推土机):-mavx256-split-unaligned-store
,但不是负载。 它还设置了 gcc8 等效 gcc7 和更早的-mprefer-avx128
(自动矢量化将仅使用 128 位 AVX,但当然内部函数仍然可以使用 256 位矢量)。-march=bdver2
(打桩机),bdver3
(压路机),bdver4
(挖掘机)。和推土机一样。他们使用软件预取自动矢量化 FPa[i] += b[i]
循环和足够的展开以每个缓存行仅预取一次!-march=znver1
(Zen):-mavx256-split-unaligned-store
但不加载,仍然仅使用 128 位进行自动矢量化,但这次没有软件预取。-march=btver2
(AMD Fam16h, aka Jaguar):既不启用拆分选项,又像 Bulldozer 系列一样自动矢量化,只有 128 位矢量 + SW 预取。-march=eden-x4
(通过带有 AVX2 的 Eden):既没有启用分割选项,但是-march
选项甚至没有启用-mavx
,以及自动矢量化使用movlps
/movhps
8 字节加载,这真的很愚蠢。至少使用movsd
而不是movlps
来打破错误的依赖。但是如果启用-mavx
,它会使用 128 位未对齐加载。这里的行为真的很奇怪/不一致,除非有一些奇怪的前端。选项(例如,作为 -march=sandybridge 的一部分启用,大概也适用于 Bulldozer 系列(-march=bdver2 是打桩机)。当编译器知道内存已对齐时,这并不能解决问题,尽管如此.
脚注 1:AMD Piledriver 有一个性能错误,这使得 256 位存储吞吐量很糟糕:根据 Agner Fog 的 microarch pdf( https://agner.org/optimize/)。此效果在 Bulldozer 或 Steamroller/Excavator.
中不存在Agner Fog 说 Bulldozer/Piledriver 上的 256 位 AVX 吞吐量一般(不是 loads/stores 特别是)通常比 128 位 AVX 差,部分原因是它无法解码 2 中的指令-2 uop 模式。 Steamroller 使 256 位接近收支平衡(如果不需要额外的洗牌)。但是寄存器-寄存器 vmovaps ymm
指令仍然只受益于推土机系列低 128 位的移动消除。
但是闭源软件或二进制发行版通常没有在每个目标架构上使用 -march=native
构建的奢侈,因此在制作可以 运行 在任何目标架构上的二进制文件时需要权衡支持 AVX CPU。在某些 CPU 上使用 256 位代码获得大幅加速通常是值得的,只要在其他 CPU 上没有灾难性的缺点。
拆分未对齐的 loads/stores 是为了避免在某些 CPU 上出现大问题。在最近的 CPU 上,它需要额外的 uop 吞吐量和额外的 ALU uops。但至少 vinsertf128 ymm, [mem], 1
不需要 Haswell/Skylake 端口 5 上的洗牌单元:它可以 运行 在任何向量 ALU 端口上。 (而且它没有微型熔断器,所以它需要 2 微秒的前端带宽。)
PS:
大多数代码不是由前沿编译器编译的,因此现在更改 "generic" 调整需要一段时间才能使用使用更新的调整编译的代码。 (当然,大多数代码只用 -O2
或 -O3
编译,而且这个选项无论如何只会影响 AVX code-gen。但不幸的是,很多人使用 -O3 -mavx2
而不是 -O3 -march=native
. 所以他们可能会错过 FMA、BMI1/2、popcnt 和他们 CPU 支持的其他东西。