【问题标题】:fastest way to fill a vector (SSE2) with a certain value. Templates friendly用某个值填充向量(SSE2)的最快方法。模板友好
【发布时间】:2012-05-23 11:45:21
【问题描述】:

我有这个模板类:

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

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

【问题讨论】:

  • 也许我误会了你,但如果D==2,你只能使用SSE2。对于D==4,您需要 AVX,而其他值根本不起作用。更好的设计可能是指定总大小,因为向量大小通常固定在某个架构上。
  • @drhirsch 我想知道在 SSE2 上设置 D>2 是否有意义。但是后来,我只是尝试编译,它可以工作。我猜 gcc 足够聪明,可以打包足够多的子向量(D==2),这对我来说很好。
  • 我尝试过一次,发现 gcc 确实为所有类型的向量生成代码,即使根本没有硬件支持 :-) 但您可能想使用 objdump 检查实际生成的代码。至少在我的情况下,它的效率非常低。
  • @drhirsch,您不需要 objudmp - 有一个 -S 编译器选项可以生成带注释的程序集输出。
  • @HristoIliev gcc -S 只会翻译一个翻译单元。而且我发现输入make 然后输入objdump 更容易:-)

标签: gcc copy initialization sse vectorization


【解决方案1】:

如果我们可以编写一次代码,只需稍作调整即可将其编译为更宽的向量,即使在自动向量化不起作用的情况下也是如此。

我得到了与@hirschhornsalz 相同的结果:当使用大于硬件支持的向量大小的向量实例化它时,代码量很大,效率低下。例如在没有 AVX512 的情况下构造 A&lt;8&gt; 会产生大量 64 位 movvmovsd 指令。它向堆栈上的本地进行一次广播,然后分别读回所有这些值,并将它们写入调用者的 struct-return 缓冲区。

对于 x86,我们可以让 gcc 为接受 double 参数(在 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 如果 gcc 将使用这种广播寻址模式将广播加载折叠到内存操作数中。

但是,gcc 也可以理解 shuffles,并且有 __builtin_shuffle 用于任意向量大小。使用全零的编译时常量掩码,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 出现内部编译器错误。向量+模板似乎需要一些工作。 (至少,它在 Ubuntu 目前正在发布的 gcc 4.9.2 中确实存在。上游可能有所改进。)

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

问题在于,除非您使用-ffast-math,否则实际添加不会被优化掉。不幸的是,-funsafe-math-optimizations 是必需的,而不仅仅是 -fno-signaling-nans。我尝试了+ 的替代方案,它们不会导致FPU 异常,例如^ (xor) 和| (or),但gcc 不会在doubles 上执行这些操作。 , 运算符不会为 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 版本,你可以在运行时将它们分派到。 (不过,您可能需要它来获得不使用 VEX 指令编码的 128 位 SSE 版本。)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-08-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-05-29
    相关资源
    最近更新 更多