我正在编写一些 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
。
答案 0 :(得分:10)
GCC的默认调整(-mtune=generic
)包括-mavx256-split-unaligned-load
和-mavx256-split-unaligned-store
,因为这在某些CPU(例如,第一代Sandybridge和某些情况下,实际上是在运行时内存未对齐的情况下。
如果不需要,请使用-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作为单个uop运行256位负载,在负载端口中需要2个周期。 (与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的,这是当时唯一的Intel AVX CPU。)
但是,如果运行时内存实际上是32字节对齐的,那么即使在Sandybridge和大多数AMD CPU 1 上,这也纯属不利。因此,使用此调整选项,您可能会因未能告知编译器对齐保证而蒙受损失。而且,如果您的循环大部分时间在对齐的内存上运行,则最好至少使用-mno-avx256-split-unaligned-load
或暗示它的调整选项来编译该编译单元。
一直使用软件进行拆分会增加成本。让硬件处理它可以使对齐的大小写完全有效(除了桩驱动器 1 上的存储除外),对齐的大小写可能比某些CPU上的软件拆分慢。因此,这是一种悲观的方法,如果确实很有可能在运行时确实对数据进行了对齐,而不是仅保证不始终在编译时进行对齐,那么这是有道理的。例如也许您有一个大多数时候都使用对齐的缓冲区调用的函数,但是您仍然希望它在罕有/少量的情况下使用未对齐的缓冲区调用时起作用。在这种情况下,即使在Sandybridge上,分段加载/存储策略也是不合适的。
缓冲区对齐通常是16字节而不是32字节,因为x86-64 glibc上的malloc
(和libstdc ++中的new
)返回16字节对齐的缓冲区(因为{{ 1}})。对于大缓冲区,指针通常在页面开始之后为16个字节,因此对于大于16的对齐方式,它总是未对齐。请使用alignof(maxalign_t) == 16
。
请注意,aligned_alloc
和-mavx
根本不会更改调整选项:-mavx2
仍会针对进行调整所有 CPU,包括实际上不能运行AVX2指令的CPU。这非常愚蠢,因为如果要调整“平均AVX2 CPU”,则应使用单个未对齐的256位负载。不幸的是,gcc无法选择这样做,并且gcc -O3 -mavx2
并不暗示-mavx2
或其他任何内容。 请参见https://gcc.gnu.org/bugzilla/show_bug.cgi?id=80568和https://gcc.gnu.org/bugzilla/show_bug.cgi?id=78762,了解功能请求具有指令集选择影响调整的功能。
这就是为什么您应该使用-mno-avx256-split-unaligned-load
来制作供本地使用的二进制文件,或者也许使用-march=native
来制作可以在各种计算机上运行的二进制文件,但是可能大多数情况下可以在较新的硬件上运行的二进制文件的原因有AVX。 (请注意,即使Skylake Pentium / Celeron CPU也没有AVX或BMI2;可能在256位执行单元或寄存器文件的上半部分有任何缺陷的CPU上,它们也会禁用VEX前缀的解码并将其作为低端出售。奔腾。)
gcc8.2的调整选项如下。 (-march=sandybridge -mtune=haswell
暗示-march=x
)。 https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html。
我使用-mtune=x
进行了编译,检查了on the Godbolt compiler explorer并查看了注释,其中包括所有隐含选项的完整转储。我提供了-O3 -fverbose-asm
函数和一个可以自动向量化的简单浮点循环,因此我们还可以研究编译器的作用。
使用_mm256_loadu/storeu_ps
(gcc8)或-mprefer-vector-width=256
(gcc7及更早版本)覆盖诸如-mno-prefer-avx128
之类的调整选项,并根据需要获得256位自动矢量化,而不仅仅是手动向量化。
-mtune=bdver3
:-mtune=generic
和-mavx256-split-unaligned-load
。可以说,随着Intel Haswell的出现越来越不适合,后来又变得越来越普遍,而我认为最近AMD CPU的缺点仍然很小。尤其是拆分未对齐的 loads ,而AMD调整选项则不启用。-store
和-march=sandybridge
:将两者分开。 (我想我已经读过IvyBridge改进了对未对齐的256位加载或存储的处理,因此它不太适合在运行时可能对齐数据的情况。)-march=ivybridge
及更高版本:均未启用拆分选项。-march=haswell
:均未启用拆分选项。 (Silvermont / Atom没有AVX)-march=knl
:均未启用拆分选项。即使使用gcc8,使用-mtune=intel
进行自动矢量化也会选择达到读/写目标数组的对齐边界,这与gcc8仅使用未对齐的常规策略不同。 (同样,在另一种情况下,软件处理总是要花钱,而让硬件来处理这种特殊情况。)-mtune=intel -mavx
(推土机):-march=bdver1
,但未加载。
它还设置了等效于gcc8的gcc7和更早的-mavx256-split-unaligned-store
(自动矢量化将仅使用128位AVX,但是固有函数仍可以使用256位矢量)。-mprefer-avx128
(打桩机),-march=bdver2
(压路机),bdver3
(挖掘机)。和推土机一样他们使用软件预取和足够的展开功能自动对FP bdver4
循环进行矢量化,从而每个缓存行仅预取一次!a[i] += b[i]
(禅宗):-march=znver1
但未加载,仍然仅使用128位自动矢量化,但这一次没有软件预取。-mavx256-split-unaligned-store
(AMD Fam16h, aka Jaguar):两个拆分选项均未启用,像Bulldozer系列一样仅通过128位向量+ SW预取自动进行了向量化。 -march=btver2
(通过带有AVX2的Via Eden):均未启用拆分选项,但-march=eden-x4
选项甚至未启用-march
,并且自动矢量化使用{{1 }} / -mavx
8字节加载,这真是愚蠢。至少使用movlps
而不是movhps
来打破错误的依赖关系。但是,如果启用movsd
,它将使用128位未对齐的负载。除非有一些奇怪的前端,否则这里的行为真的很奇怪/不一致。
选项(例如,作为-march = sandybridge的一部分启用,大概也适用于Bulldozer系列(-march = bdver2是桩驱动程序)。但是,当编译器知道内存已对齐时,这不能解决问题。 / p>
脚注1:AMD Piledriver的性能错误使256位存储吞吐量变得可怕:根据Agner Fog的microarch pdf(https://agner.org/optimize/),甚至movlps
对齐的存储每17到20个时钟运行一次。推土机或Steamroller /挖掘机中没有这种效果。
Agner Fog说,Bulldozer / Piledriver上的256位AVX吞吐量通常(而不是专门加载/存储)通常比128位AVX差,部分原因是它无法以2-2 uop模式解码指令。 Steamroller使256位接近收支平衡(如果它不需要花费额外的洗牌)。但是寄存器-寄存器-mavx
指令仍然只能从推土机系列中低128位的消除中受益。
但是,闭源软件或二进制发行版通常没有在每个目标体系结构上都使用vmovaps [mem], ymm
进行构建的奢侈之处,因此在制作可以在任何支持AVX的CPU上运行的二进制文件时要进行权衡。只要在其他CPU上不存在灾难性的不利影响,通常在某些CPU上使用256位代码实现大幅提速是值得的。
拆分未对齐的加载/存储是为了避免某些CPU出现大问题的尝试。在最近的CPU上,它会产生额外的uop吞吐量和额外的ALU uops。但是至少vmovaps ymm
不需要Haswell / Skylake的端口5上的随机播放单元:它可以在任何矢量ALU端口上运行。 (而且它没有微熔丝,所以它的前端带宽要2微克。)
PS:
大多数代码不是由最先进的编译器编译的,因此现在更改“通用”调优将花费一些时间,然后才能使用经过更新的调优编译的代码。 (当然,大多数代码仅使用-march=native
或vinsertf128 ymm, [mem], 1
进行编译,无论如何,此选项仅会影响AVX代码生成。但是很不幸,许多人使用-O2
而不是-O3
因此,他们可能会错过FMA,BMI1 / 2,popcnt以及其CPU支持的其他功能。
答案 1 :(得分:5)
GCC的通用调整splits unaligned 256-bit loads可帮助较旧的处理器。 (我相信,后续的更改可避免在常规调整中分散负载。)
您可以使用-mtune=intel
或-mtune=skylake
之类的东西来调整最新的Intel CPU,并且将按预期获得一条指令。