gcc之用特定值填充向量 (SSE2) 的最快方法。模板友好

落叶无声 阅读:49 2024-11-01 17:39:52 评论:0

我有这个模板类:

template<size_t D> 
struct A{ 
    double v_sse __attribute__ ((vector_size (8*D))); 
    A(double val){ 
        //what here? 
    } 
}; 

val 的副本填充 v_sse 字段的最佳方法是什么?因为我使用向量,所以我可以使用 gcc SSE2 内在函数。

请您参考如下方法:

如果我们可以编写一次代码,然后通过一个小的调整就可以为更广泛的向量编译它,即使在自动向量化不能解决问题的情况下也是如此。

我得到了与@hirschhornsalz 相同的结果:使用大于 HW 支持的矢量大小的矢量对其进行实例化时,代码量大、效率低下。例如施工 A<8>没有 AVX512 会产生大量 64 位 movvmovsd指示。它在堆栈上向本地广播,然后分别读回所有这些值,并将它们写入调用者的结构返回缓冲区。

对于 x86,我们可以让 gcc 为采用 double 的函数发出最佳广播 arg(在 xmm0 中),并根据标准调用约定返回一个向量(在 x/y/zmm0 中):

  • SSE2:unpckpd xmm0, xmm0
  • SSE3:movddup xmm0, xmm0
  • AVX:vmovddup xmm0, xmm0 / vinsertf128 ymm0, ymm0, xmm0, 1
    (AVX1 只包含 vbroadcastsd ymm, m64 形式,这将 如果在调用内存中的数据时内联,大概会被使用)
  • AVX2:vbroadcastsd ymm0, xmm0
  • AVX512:vbroadcastsd zmm0, xmm0 . (请注意,AVX512 可以从内存中动态广播:
    VADDPD zmm1 {k1}{z}, zmm2, zmm3/m512/m64bcst{er}
    {k1}{z}意味着它可以使用掩码寄存器作为结果的合并或零掩码。
    m64bcst表示要广播的 64 位内存地址。
    {er}意味着可以为这条指令覆盖 MXCSR 舍入模式。
    IDK if gcc 将使用此广播寻址模式将广播负载折叠到内存操作数中。

然而,gcc 也理解随机播放,并且有 __builtin_shuffle对于任意向量大小。使用全零的编译时常量掩码,洗牌变成广播,gcc 会使用最适合该工作的指令。

typedef int64_t v4di __attribute__ ((vector_size (32))); 
typedef double  v4df __attribute__ ((vector_size (32))); 
v4df vecinit4(double v) { 
    v4df v_sse; 
    typeof (v_sse) v_low = {v}; 
    v4di shufmask = {0}; 
    v_sse = __builtin_shuffle (v_low, shufmask ); 
    return v_sse; 
} 

在模板函数中,gcc 4.9.2 似乎无法识别两个向量具有相同的宽度和元素数量,并且掩码是一个 int 向量。即使没有实例化模板也会出错,所以也许这就是类型有问题的原因。如果我复制该类并将其取消模板化为特定的矢量大小,一切都会完美无缺。

template<int D> struct A{ 
    typedef double  dvec __attribute__ ((vector_size (8*D))); 
    typedef int64_t ivec __attribute__ ((vector_size (8*D))); 
    dvec v_sse;  // typeof(v_sse) is buggy without this typedef, in a template class 
    A(double v) { 
#ifdef SHUFFLE_BROADCAST  // broken on gcc 4.9.2 
    typeof(v_sse)  v_low = {v}; 
    //int64_t __attribute__ ((vector_size (8*D))) shufmask = {0}; 
    ivec shufmask = {0, 0}; 
    v_sse = __builtin_shuffle (v_low, shufmask);  // no idea why this doesn't compile 
#else 
    typeof (v_sse) zero = {0, 0}; 
    v_sse = zero + v;  // doesn't optimize away without -ffast-math 
#endif 
    } 
}; 
 
/*  doesn't work: 
double vec2val  __attribute__ ((vector_size (16))) = {v, v}; 
double vec4val  __attribute__ ((vector_size (32))) = {v, v, v, v}; 
v_sse = __builtin_choose_expr (D == 2, vec2val, vec4val); 
*/ 

在使用 -O0 编译时,我设法让 gcc 进入内部编译器错误. vectors + templates 似乎需要一些工作。 (至少,它在 Ubuntu 当前发布的 gcc 4.9.2 中确实存在。上游可能有所改进。)

我的第一个想法是,当您使用带有向量和标量的运算符时,gcc 会隐式广播,因为 shuffle 无法编译,所以我将其留作后备。因此,例如,将标量添加到全零向量即可达到目的。

问题是除非你使用 -ffast-math,否则实际的添加不会被优化掉. -funsafe-math-optimizations不幸的是需要,而不仅仅是 -fno-signaling-nans .我尝试了 + 的替代品不会导致 FPU 异常,例如 ^ (异或)和 | (或),但 gcc 不会在 double 上执行这些操作秒。 ,运算符不会为 scalar , vector 生成矢量结果.

这可以通过使用简单的初始化列表专门化模板来解决。如果您无法获得一个好的通用构造函数来工作,我建议您省略定义,这样当没有专门化时您会遇到编译错误。

#ifndef NO_BROADCAST_SPECIALIZE 
// specialized versions with initializer lists to work efficiently even without -ffast-math 
// inline keyword prevents an actual definition from being emitted. 
template<> inline A<2>::A (double v) { 
    typeof (v_sse) val = {v, v}; 
    v_sse = val; 
} 
template<> inline A<4>::A (double v) { 
    typeof (v_sse) val = {v, v, v, v}; 
    v_sse = val; 
} 
template<> inline A<8>::A (double v) { 
    typeof (v_sse) val = {v, v, v, v, v, v, v, v}; 
    v_sse = val; 
} 
template<> inline A<16>::A (double v) { // AVX1024 or something may exist someday 
    typeof (v_sse) val = {v, v, v, v, v, v, v, v, v, v, v, v, v, v, v, v}; 
    v_sse = val; 
} 
#endif 

现在,测试结果:

// vecinit4 (from above) included in the asm output too. 
// instantiate the templates 
A<2> broadcast2(double val) { return A<2>(val); } 
A<4> broadcast4(double val) { return A<4>(val); } 
A<8> broadcast8(double val) { return A<8>(val); } 

编译器输出(去除汇编器指令):

g++ -DNO_BROADCAST_SPECIALIZE  -O3 -Wall -mavx512f -march=native vec-gcc.cc -S -masm=intel -o- 
 
_Z8vecinit4d: 
    vbroadcastsd    ymm0, xmm0 
    ret 
_Z10broadcast2d: 
    vmovddup        xmm1, xmm0 
    vxorpd  xmm0, xmm0, xmm0 
    vaddpd  xmm0, xmm1, xmm0 
    ret 
_Z10broadcast4d: 
    vbroadcastsd    ymm1, xmm0 
    vxorpd  xmm0, xmm0, xmm0 
    vaddpd  ymm0, ymm1, ymm0 
    ret 
_Z10broadcast8d: 
    vbroadcastsd    zmm0, xmm0 
    vpxorq  zmm1, zmm1, zmm1 
    vaddpd  zmm0, zmm0, zmm1 
    ret 
 
 
g++ -O3 -Wall -mavx512f -march=native vec-gcc.cc -S -masm=intel -o- 
# or   g++ -ffast-math -DNO_BROADCAST_SPECIALIZE blah blah. 
 
_Z8vecinit4d: 
    vbroadcastsd    ymm0, xmm0 
    ret 
_Z10broadcast2d: 
    vmovddup        xmm0, xmm0 
    ret 
_Z10broadcast4d: 
    vbroadcastsd    ymm0, xmm0 
    ret 
_Z10broadcast8d: 
    vbroadcastsd    zmm0, xmm0 
    ret 

请注意,如果您不将其模板化,而是在您的代码中仅使用一个向量大小,则 shuffle 方法应该可以正常工作。因此,从 SSE 更改为 AVX 就像在一个地方将 16 更改为 32 一样简单。但是随后您需要多次编译同一个文件以生成一个 SSE 版本和一个 AVX 版本,您可以在运行时将其分派(dispatch)到这些版本。 (尽管如此,您可能仍然需要它来拥有不使用 VEX 指令编码的 128 位 SSE 版本。)


标签:程序员
声明

1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;3.作者投稿可能会经我们编辑修改或补充。

关注我们

一个IT知识分享的公众号