强制AVX指令使用SSE指令的方法

6

非常遗憾我使用的是AMD piledriver处理器,该处理器似乎存在AVX指令问题:

256位AVX寄存器的内存写入速度异常缓慢。测量的吞吐量比上一代(Bulldozer)慢5至6倍,比两个128位的写入慢8至9倍。

根据我的经验,我发现mm256内置函数比mm128慢得多,我认为这是由于上述原因造成的。

然而,我真的想为最新的AVX指令编码,同时仍能以合理的速度在我的机器上进行测试。有没有办法强制mm256内置函数使用SSE指令呢? 我正在使用VS 2015。

如果没有简单的方法,那么考虑一种困难的方法。可以将<immintrin.h>替换为自定义的头文件,其中包含自己的内置函数定义,可以编码成使用SSE吗?不确定这是否可行,在尝试这些工作之前希望有更简单的方法。


我认为不会有这样的事情。他们不会为了一个特定的处理器彻底改造他们的编译器。(只有Piledriver存在那个缺陷) - Mysticial
当你引用某些内容时,应该给出参考。是的,这个问题有解决方案。使用 Agner Fog 的向量类。使用 AVX 向量,例如 Vec8f 并使用 -D__SSE4_2__ -D__XOP__ 进行编译。 - Z boson
你使用256位是否会变慢?你可能有对齐问题吗? - Cory Nelson
2个回答

6
请使用 Agner Fog 的向量类库,并将以下内容添加到 Visual Studio 的命令行:-D__SSE4_2__ -D__XOP__
然后使用 AVX 大小的向量,例如 Vec8f 用于八个浮点数。当您没有启用 AVX 进行编译时,它将使用文件 vectorf256e.h,该文件使用两个 SSE 寄存器模拟 AVX。例如,Vec8f 继承自 Vec256fe,其开头如下:
class Vec256fe {
protected:
    __m128 y0;                         // low half
    __m128 y1;                         // high half

如果您使用/arch:AVX -D__XOP__编译,VCL将使用文件vectorf256.h和一个AVX寄存器。然后,只需更改编译器开关,您的代码即可在AVX和SSE上运行。
如果您不想使用XOP,请勿使用-D__XOP__
正如Peter Cordes在他的回答中指出的那样,如果你的目标仅是避免256位的加载/存储,那么你仍然可能需要使用VEX编码的指令(虽然这在某些特殊情况下可能没有区别)。你可以像这样使用向量类来实现。
Vec8f a;
Vec4f lo = a.get_low();  // a is a Vec8f type
Vec4f hi = a.get_high();
lo.store(&b[0]);         // b is a float array
hi.store(&b[4]);

然后使用/arch:AVX -D__XOP__编译。

另一个选择是使用Vecnf的一个源文件,然后执行

//foo.cpp
#include "vectorclass.h"
#if SIMDWIDTH == 4
typedef Vec4f Vecnf;
#else
typedef Vec8f Vecnf;
#endif  

并像这样编译

cl /O2 /DSIMDWIDTH=4                     foo.cpp /Fofoo_sse
cl /O2 /DSIMDWIDTH=4 /arch:AVX /D__XOP__ foo.cpp /Fofoo_avx128
cl /O2 /DSIMDWIDTH=8 /arch:AVX           foo.cpp /Fofoo_avx256

这将使用一个源文件创建三个可执行文件。您可以使用/c编译它们,而不是链接它们,然后制作一个CPU分发器。我使用了XOP和avx128,因为我认为除了在AMD上使用之外,没有什么好的理由使用avx128。

我在想如何做相反的操作。在向量类库中将__m256推入Vec8fe。是的,这听起来没有意义,但我需要这个情况。 - Royi
@Royii 你为什么需要这个案例?如果你有__m256,那就意味着你已经启用了AVX编译器,然后VCL将使用Vec8f而不是Vec8fe - Z boson
因为在某些情况下,我希望在我的系统中有两个不同的代码。一个用于SSE,另一个用于AVX。VCL的问题是它只处理其中一个。我希望能够强制它使用Vec8f的AVX和Vec4f的SSE。 - Royi
@Royi,制作一个CPU分派器,并根据指令集选择代码路径。在某些情况下,使用Vec8fe可能比两次使用Vec4f效果更差。我避免使用模拟类型。 - Z boson
我在我的程序中遇到了一个问题。我有一个只使用SSE Intrinsics构建的函数,还有一个使用AVX Intrinsics构建的相同函数。这不是编译器决定如何编译的问题。我希望它以这种方式编写。 - Royi
@Royi,在SO上提出一个问题。这些评论很难理解问题。一个VCL + 内在问题是有趣的。我不是唯一使用VCL的人,所以其他人也可以回答你的问题。 - Z boson

3
你不想使用SSE指令。你想要的是将256b存储作为两个单独的128b存储完成,仍然使用VEX编码的128b指令。即128b AVX vmovups

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


您可以使用类似的宏覆盖正常的256b存储内在函数:

// maybe enable this for all BD family CPUs?

#if defined(__bdver2) | defined(PILEDRIVER) | defined(SPLIT_256b_STORES)
   #define _mm256_storeu_ps(addr, data) do{ \
      _mm_storeu_ps( ((float*)(addr)) + 0, _mm256_extractf128_ps((data),0)); \
      _mm_storeu_ps( ((float*)(addr)) + 4, _mm256_extractf128_ps((data),1)); \
   }while(0)
#endif

gcc为Piledriver定义了__bdver2(Bulldozer版本2) (-march=bdver2)。

您可以对(aligned) _mm256_store_ps执行相同操作,或者始终使用unaligned intrinsic。

编译器将_mm256_extractf128(data,0)优化为简单的转换。也就是说,它应该只编译为

vmovups       [rdi], xmm0         ; if data is in xmm0 and addr is in rdi
vextractf128  [rdi+16], xmm0, 1

然而,在godbolt上测试表明gcc和clang很蠢,需要提取到寄存器并然后存储。ICC会正确生成两个指令序列。

由于在AMD上,AVX基本上是作为SSE两次硬件仿真的,那么使用非VEX编码指令有什么问题吗?我能想到的唯一优点是使用AVX指令但分割负载/存储可以使用更少的寄存器和指令缓存中的指令。 - Z boson
我想,由于非对齐的载荷在非VEX编码的指令中不能被折叠,这就是使用VEX编码指令的原因之一。 - Z boson
@Zboson:是的,根据我所读的(例如Agner Fog),在AMD上使用256b向量通常没有什么优势。使用VEX编码指令的128b向量通常是最好的选择。这个答案对于使用Piledriver机器进行开发的AVX软件的开发/调试非常有用。您可以使用256b内部函数而不会触发256b存储性能错误。因此,在Piledriver上,您将获得与使用_mm_* 128b内部函数编写代码相同的速度,但希望在Intel硬件上获得更快的速度。 - Peter Cordes
@Volatile:不行,你不能。正确的术语是“溢出”而不是“溢出”,用于描述编译器在无法容纳所有本地变量和临时变量的寄存器时所做的操作(即将其“溢出到堆栈中”)。但我不知道它是否可以假定堆栈是32字节对齐的,因此gcc-mavx256-split-unaligned-store可能仍会生成2条指令来进行存储。在大多数代码中,溢出很少发生(例如每次调用几次,而不是每次循环迭代)。由于您试图避免的问题只是一个约17个周期的性能问题,而不是段错误,因此您可能没问题。 - Peter Cordes
@Volatile:我刚刚测试了一下,编写了一个使用17个__m256变量的函数。http://goo.gl/3cwpgz。它将它们用作FP求和的累加器,因此溢出在此处循环内部发生。gcc对齐堆栈,然后创建堆栈帧。此外,我刚刚发现,在GNU C中,运算符被重载为__m256变量,因此您可以使用ymm0 += ymm1将它们相加以获得vaddps指令。(gcc / clang,但不是icc13)。此外,即使在-march = sandybridge下,其中vxorps每个时钟周期运行4次,vmovaps r,r每个时钟周期运行3次,gcc也只执行一次vxorps,然后使用vmovaps将其他寄存器清零。 - Peter Cordes
显示剩余2条评论

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