【问题标题】:Why doesn't gcc resolve _mm256_loadu_pd as single vmovupd?为什么 gcc 不将 _mm256_loadu_pd 解析为单个 vmovupd?
【发布时间】:2019-03-08 15:58:30
【问题描述】:

我正在编写一些 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 编译等效代码。

我在 Ubuntu 18.04 x86-64 上运行 gcc (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0

【问题讨论】:

  • 您需要选择合适的 cpu 型号进行优化,例如-march=haswellznver1 将生成单指令版本。

标签: gcc assembly vectorization simd avx


【解决方案1】:

GCC 的通用调整 splits unaligned 256-bit loads 以帮助旧处理器。 (我相信,后续更改避免了在通用调优中拆分负载。)

您可以使用-mtune=intel-mtune=skylake 之类的方式针对更新的Intel CPU 进行调优,并且您将获得一条符合预期的指令。

【讨论】:

  • 郑重声明,即使icc 也这样做了,所以不要责怪 AMD。
  • 使用两个 128 位单元实现 AVX2 的 AMD 处理器比此类 Intel 处理器更常见(在 AMD 中)。尽管。这就是-mtune=intel 在 GCC 中禁用此优化的原因。
  • 拆分未对齐的 loads 是为了帮助 Sandybridge。 AMD 调整只拆分未对齐的 stores。 (将这两个拆分都作为tune=generic 的一部分越来越不合适了。)
  • @PeterCordes 我应该把这个答案变成一个社区维基,这样你就可以在这里粘贴你的答案吗?不幸的是,我不能不接受我的回答。
  • 不,没关系。 @Emanuele 希望您的建议能接受我的回答。但如果不是,你的回答足够短,人们仍然会看到我的。
【解决方案2】:

GCC 的默认调优 (-mtune=generic) 包括 -mavx256-split-unaligned-load-mavx256-split-unaligned-store,因为这会在某些 CPU(例如第一代 Sandybridge 和一些 AMD CPU)上略微加速内存在运行时实际上未对齐的情况。

如果您不想要这个,请使用-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)。

英特尔 Sandybridge 将 256 位加载作为单个 uop 运行,在加载端口中需要 2 个周期。 (与将所有 256 位向量指令解码为 2 个单独的微指令的 AMD 不同。)Sandybridge 存在未对齐的 256 位加载问题(如果地址在运行时实际上未对齐)。我不知道细节,也没有找到关于减速到底是什么的具体信息。也许是因为它使用了带有 16 字节存储区的存储区缓存?但 IvyBridge 可以更好地处理 256 位负载,并且仍然具有存储区缓存。

根据 GCC 邮件列表中关于实现该选项的代码 (https://gcc.gnu.org/ml/gcc-patches/2011-03/msg01847.html) 的消息,“它将某些 SPEC CPU 2006 基准测试速度提高了 6%。”(我认为这是Sandybridge,当时唯一的英特尔 AVX CPU。)


但如果内存在运行时实际上是 32 字节对齐的,即使在 Sandybridge 和大多数 AMD CPU1 上,这也是纯粹的缺点。因此,使用此调整选项,您可能会因为未能告诉编译器对齐保证而失败。如果您的循环大多数时间都在对齐的内存上运行,那么您最好至少使用 -mno-avx256-split-unaligned-load 或暗示这一点的调整选项编译该编译单元。

在软件中拆分始终会带来成本。让硬件处理它使对齐的情况非常有效(除了 Piledriver1 上的存储),未对齐的情况可能比在某些 CPU 上使用软件拆分慢。所以这是一种悲观的方法,如果数据真的很可能在运行时没有对齐,而不是不能保证在编译时总是对齐,那么它是有意义的。例如也许您有一个大多数时间使用对齐缓冲区调用的函数,但您仍然希望它适用于使用未对齐缓冲区调用的罕见/小情况。在这种情况下,即使在 Sandybridge 上,拆分加载/存储策略也不合适。

缓冲区通常是 16 字节对齐但不是 32 字节对齐的,因为 x86-64 glibc 上的 malloc(以及 libstdc++ 中的 new)返回 16 字节对齐的缓冲区(因为 alignof(maxalign_t) == 16)。对于大缓冲区,指针通常在页面开始后 16 个字节,因此对于大于 16 的对齐,它总是未对齐。请改用aligned_alloc


请注意,-mavx-mavx2 根本不会更改 调整 选项gcc -O3 -mavx2 仍会调整 所有 CPU ,包括那些实际上不能运行 AVX2 指令的。这很愚蠢,因为如果调整“平均 AVX2 CPU”,您应该使用单个未对齐的 256 位负载。不幸的是,gcc 没有选择这样做,-mavx2 并不暗示-mno-avx256-split-unaligned-load 或任何东西。 请参阅 https://gcc.gnu.org/bugzilla/show_bug.cgi?id=80568https://gcc.gnu.org/bugzilla/show_bug.cgi?id=78762,了解指令集选择影响调整的功能请求

这就是为什么您应该使用-march=native 制作二进制文件以供本地使用,或者也许使用-march=sandybridge -mtune=haswell 制作可以在各种机器上运行的二进制文件,但可能主要在具有 AVX 的较新硬件上运行。 (请注意,即使 Skylake Pentium/Celeron CPU 也没有 AVX 或 BMI2;可能在 256 位执行单元或寄存器文件的上半部分存在任何缺陷的 CPU 上,它们会禁用 VEX 前缀的解码并将其作为低端出售奔腾。)


gcc8.2 的调优选项如下。 (-march=x 暗示 -mtune=x)。 https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html.

我通过使用-O3 -fverbose-asm 编译检查了on the Godbolt compiler explorer,并查看了包含所有隐含选项的完整转储的 cmets。我包含了_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 CPU 的缺点仍然很小。尤其是拆分未对齐的负载,这是 AMD 调整选项无法启用的。
  • -march=sandybridge-march=ivybridge:将两者分开。 (我想我已经读过 IvyBridge 改进了对未对齐的 256 位加载或存储的处理,因此它不太适合数据可能在运行时对齐的情况。)
  • -march=haswell 及更高版本:均未启用拆分选项。
  • -march=knl:拆分选项均未启用。 (Silvermont/Atom 没有 AVX)
  • -mtune=intel:没有启用拆分选项。即使使用 gcc8,-mtune=intel -mavx 的自动向量化也会选择达到读/写目标数组的对齐边界,这与 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 位进行自动矢量化,但这次没有 SW 预取。
  • -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 的微架构 pdf (https://agner.org/optimize/),即使 vmovaps [mem], ymm 对齐的存储每 17 到 20 个时钟运行一个时钟。此效果在推土机或压路机/挖掘机中不存在。

Agner Fog 表示,Bulldozer/Piledriver 上的 256 位 AVX 吞吐量(不是专门加载/存储)通常比 128 位 AVX 差,部分原因是它无法以 2-2 uop 模式解码指令。 Steamroller 使 256 位接近收支平衡(如果它不花费额外的洗牌)。但是 register-register vmovaps ymm 指令仍然只能从 Bulldozer 系列的低 128 位的 mov 消除中受益。

但闭源软件或二进制发行版通常无法在每个目标架构上都使用-march=native 进行构建,因此在制作可在任何支持 AVX 的 CPU 上运行的二进制文件时需要进行权衡。只要在其他 CPU 上没有灾难性的缺点,在某些 CPU 上使用 256 位代码获得大幅加速通常是值得的。

拆分未对齐的加载/存储是为了避免在某些 CPU 上出现大问题。在最近的 CPU 上,它需要额外的 uop 吞吐量和额外的 ALU uop。但至少vinsertf128 ymm, [mem], 1 不需要 Haswell/Skylake 端口 5 上的 shuffle 单元:它可以在任何矢量 ALU 端口上运行。 (而且它没有微熔断器,因此它会消耗 2 微秒的前端带宽。)


PS:

大多数代码不是由最前沿的编译器编译的,因此现在更改“通用”调优需要一段时间才能使用经过更新的调优编译的代码。 (当然,大多数代码仅使用-O2-O3 编译,并且此选项无论如何只会影响AVX 代码生成。但不幸的是,许多人使用-O3 -mavx2 而不是-O3 -march=native。所以他们可能会错过FMA 、BMI1/2、popcnt 和其他他们的 CPU 支持的东西。

【讨论】:

猜你喜欢
  • 1970-01-01
  • 2013-05-01
  • 2013-05-11
  • 1970-01-01
  • 2014-07-28
  • 1970-01-01
  • 2018-01-25
相关资源
最近更新 更多