为什么gcc不能将_mm256_loadu_pd解析为单个vmovupd?

14

我正在编写一些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将生成单指令版本。 - Jester
2个回答

18
GCC的默认调优(-mtune=generic)包括-mavx256-split-unaligned-load-mavx256-split-unaligned-store,因为在某些情况下(例如第一代Sandybridge和一些AMD CPU)当内存在运行时实际上是未对齐的时,会给一些CPU带来轻微的加速效果。
如果你不想要这个功能,请使用-O3 -mno-avx256-split-unaligned-load -mno-avx256-split-unaligned-store,或者更好地使用-mtune=haswell。或者使用-march=native来优化你自己的计算机。没有“通用的AVX2”调优。(来源: https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html)。
Intel Sandybridge将256位载入作为一个单独的微操作,在一个载入端口中需要2个周期。(与AMD不同,它将所有256位矢量指令解码为2个单独的微操作。) Sandybridge在处理未对齐的256位载入时存在问题(如果地址在运行时实际上未对齐)。我不知道具体细节,并且没有找到太多关于减速确切原因的信息。也许是因为它使用了分段缓存,其中有16字节的段? 但IvyBridge能更好地处理256位载入,而且仍然有分段缓存。
根据有关实现该选项的代码的GCC邮件列表消息(https://gcc.gnu.org/ml/gcc-patches/2011-03/msg01847.html),"它可以将一些SPEC CPU 2006基准测试加速高达6%。"(我认为这是针对当时唯一存在的英特尔AVX CPU Sandybridge的情况。)
但如果内存在运行时实际上是32字节对齐的,即使在Sandybridge和大多数AMD CPU上,这也是纯负面的影响。因此,使用此调优选项,您可能会因未告知编译器有关对齐保证而失去优势。如果您的循环大部分时间都在对齐的内存上运行,则最好使用 -mno-avx256-split-unaligned-load 或暗示该选项的调优选项来编译至少编译单元。
在软件中进行拆分会一直产生成本。让硬件处理使得对齐的情况完全有效(Piledriver除外),而在某些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指令的CPU。 这很愚蠢,因为如果要针对“平均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前缀的解码并将其作为低端Pentium出售。)


gcc8.2的优化选项如下。(-march=x意味着-mtune=x)。https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html

我使用-O3 -fverbose-asm编译,在Godbolt编译器探索器上查看包含所有隐含选项的注释。 我包括了_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 (Bulldozer): -mavx256-split-unaligned-store选项启用,但不包括加载。此外,它还设置了与gcc7及更早版本等效的gcc8选项-mprefer-avx128(自动矢量化仅使用128位AVX,但当然内部函数仍然可以使用256位向量)。
  • -march=bdver2(Piledriver),bdver3(Steamroller),bdver4(Excavator)。与Bulldozer相同。它们通过软件预取和足够的展开来自动矢量化FP a[i] += b[i]循环,以每个缓存行仅预取一次!
  • -march=znver1(Zen):-mavx256-split-unaligned-store但不包括加载,仍然只使用128位自动矢量化,但这次没有SW预取。
  • -march=btver2AMD Fam16h,又名Jaguar):未启用任何分裂选项,像Bulldozer-family一样自动矢量化,仅使用128位向量+ SW预取。
  • -march=eden-x4(带有AVX2的Via Eden):未启用任何分裂选项,但-march选项甚至不启用-mavx,自动矢量化使用movlps / movhps 8字节加载,这真的很愚蠢。至少使用movsd而不是movlps来打破假依赖关系。但如果启用-mavx,它将使用128位非对齐加载。真的很奇怪/不一致的行为,除非有一些奇怪的前端。

    选项(例如作为-march = sandybridge的一部分启用,可能也适用于Bulldozer-family(-march = bdver2是piledriver)。但当编译器知道内存对齐时,这并不能解决问题。


注1:AMD Piledriver存在性能缺陷,使得256位存储吞吐量非常糟糕:即使对齐存储vmovaps [mem], ymm每17到20个时钟运行一次,根据Agner Fog的微架构pdf(https://agner.org/optimize/)。这种效果在Bulldozer或Steamroller / Excavator中不存在。
Agner Fog表示,Bulldozer / Piledriver上的256位AVX吞吐量通常比128位AVX差,部分原因是它无法以2-2 uop模式解码指令。 Steamroller使256位接近平衡(如果不需要额外的洗牌)。但是,在Bulldozer系列中,寄存器寄存器vmovaps ymm指令仍然只受益于低128位的mov消除。
但是,闭源软件或二进制发行版通常没有在每个目标体系结构上使用-march = native的便利,因此在制作可以在任何支持AVX的CPU上运行的二进制文件时存在权衡。只要在其他CPU上没有灾难性的副作用,通过256位代码在某些CPU上获得大的加速通常是值得的。
分离未对齐的加载/存储是为了避免某些CPU上出现大问题的尝试。这会在最近的CPU上增加额外的uop吞吐量和额外的ALU uops。但至少在Haswell/Skylake上,vinsertf128 ymm,[mem],1不需要端口5上的洗牌单元:它可以在任何向量ALU端口上运行。(并且它不会微聚合,因此它会消耗2个前端带宽的uops。)

PS:

大多数代码并非由最新的编译器编译,因此现在更改“通用”调优需要一段时间,直到使用更新调优编译的代码。 (当然,大多数代码只使用-O2-O3进行编译,而此选项仅影响AVX代码生成。但是,许多人不幸地使用-O3 -mavx2而不是-O3 -march=native。因此,他们可能会错过FMA、BMI1/2、popcnt和其他CPU支持的功能。


@Emanuele:这个默认调整选项多年来一直是我心中的一个小烦恼,因为“不保证对齐”并不总是意味着“在运行时可能实际上未对齐”,而这看起来是放置一个规范答案的好地方。我之前在GCC最高兼容多种架构指令集强制AVX内部函数使用SSE指令以及其他问题的评论中提到过它。 - Peter Cordes

6
GCC 的通用调优在旧处理器上会拆分不对齐的 256 位载入(详见此处)。(后续更改避免在通用调优中拆分载入,我相信。)
你可以使用类似 -mtune=intel-mtune=skylake 的方式为更新的 Intel CPU 进行调优,从而得到预期的单条指令。

就记录而言,即使是icc也会这样做,所以不要把它归咎于AMD。 - Jester
使用两个128位单元实现AVX2的AMD处理器比此类Intel处理器更常见。这就是为什么在GCC中 -mtune=intel 会禁用此优化的原因。 - Florian Weimer
将非对齐负载拆分以帮助Sandybridge。 AMD调谐仅拆分不对齐的存储。(将这两个分割作为tune = generic的一部分变得越来越不适合。) - Peter Cordes
@PeterCordes 我应该将这个答案转换为社区维基,以便您可以在此粘贴您的答案吗?不幸的是,我无法取消接受我的答案。 - Florian Weimer
不,没关系。@Emanuele希望能采纳我的答案,但如果没有,你的答案很短,人们仍然会看到我的答案。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接