如何编写便携式simd代码用于复数乘法约简

时间:2017-07-25 09:13:54

标签: c++ c gcc simd avx

我想编写快速simd代码来计算复杂数组的乘法减少。在标准C中,这是:

#include <complex.h>
complex float f(complex float x[], int n ) {
   complex float p = 1.0;
   for (int i = 0; i < n; i++)
      p *= x[i];
   return p;
}

n最多为50个。

Gcc无法自动矢量化复数乘法,但是,我很乐意假设gcc编译器,如果我知道我想要定位sse3,我可以关注How to enable sse3 autovectorization in gcc并写:

typedef float v4sf __attribute__ ((vector_size (16)));
typedef union {
  v4sf v;
  float e[4];
} float4
typedef struct {
  float4 x;
  float4 y;
} complex4;
static complex4 complex4_mul(complex4 a, complex4 b) {
  return (complex4){a.x.v*b.x.v -a.y.v*b.y.v, a.y.v*b.x.v + a.x.v*b.y.v};
}
complex4 f4(complex4 x[], int n) {
  v4sf one = {1,1,1,1};
  complex4 p = {one,one};
  for (int i = 0; i < n; i++) p = complex4_mul(p, x[i]);
  return p;
}

这确实使用gcc生成快速矢量化汇编代码。虽然你仍然需要将你的输入填充到4的倍数。你得到的程序集是:

.L3:
    vmovaps xmm0, XMMWORD PTR 16[rsi]
    add     rsi, 32
    vmulps  xmm1, xmm0, xmm2
    vmulps  xmm0, xmm0, xmm3
    vfmsubps        xmm1, xmm3, XMMWORD PTR -32[rsi], xmm1
    vmovaps xmm3, xmm1
    vfmaddps        xmm2, xmm2, XMMWORD PTR -32[rsi], xmm0
    cmp     rdx, rsi
    jne     .L3

但是,它专为精确的simd指令集而设计,对于avx2或avx512而言并非最佳,例如您需要更改代码。

  

如何编写gcc将产生最佳效果的C或C ++代码   编译为sse,avx2或avx512中的任何一个时的代码?也就是说,对于每个不同宽度的SIMD寄存器,您是否总是需要手动编写单独的函数?

     

是否有任何开源库可以让这更容易?

3 个答案:

答案 0 :(得分:12)

以下是使用Eigen library

的示例
#include <Eigen/Core>
std::complex<float> f(const std::complex<float> *x, int n)
{
    return Eigen::VectorXcf::Map(x, n).prod();
}

如果使用clang或g ++和sse或avx enabled(和-O2)编译它,你应该得到相当不错的机器代码。它也适用于其他一些架构,如Altivec或NEON。如果您知道x的第一个条目已对齐,则可以使用MapAligned代替Map

如果您在编译时碰巧知道矢量的大小,那么您可以获得更好的代码:

template<int n>
std::complex<float> f(const std::complex<float> *x)
{
    return Eigen::Matrix<std::complex<float>, n, 1> >::MapAligned(x).prod();
}

注意:上述函数直接对应于OP的函数f。 然而,正如@PeterCordes所指出的那样,存储交错复数的通常是不好的,因为这将需要大量的重排以进行乘法。相反,应该以一种可以直接加载一个数据包的方式存储实部和虚部。

编辑/附录:要实现像复数乘法一样的数组结构,您实际上可以编写如下内容:

typedef Eigen::Array<float, 8, 1> v8sf; // Eigen::Array allows element-wise standard operations
typedef std::complex<v8sf> complex8;
complex8 prod(const complex8& a, const complex8& b)
{
    return a*b;
}

或更通用(使用C ++ 11):

template<int size, typename Scalar = float> using complexX = std::complex<Eigen::Array<Scalar, size, 1> >;

template<int size>
complexX<size> prod(const complexX<size>& a, const complexX<size>& b)
{
    return a*b;
}

使用-mavx -O2编译时,会编译为类似的内容(使用g ++ - 5.4):

    vmovaps 32(%rsi), %ymm1
    movq    %rdi, %rax
    vmovaps (%rsi), %ymm0
    vmovaps 32(%rdi), %ymm3
    vmovaps (%rdi), %ymm4
    vmulps  %ymm0, %ymm3, %ymm2
    vmulps  %ymm4, %ymm1, %ymm5
    vmulps  %ymm4, %ymm0, %ymm0
    vmulps  %ymm3, %ymm1, %ymm1
    vaddps  %ymm5, %ymm2, %ymm2
    vsubps  %ymm1, %ymm0, %ymm0
    vmovaps %ymm2, 32(%rdi)
    vmovaps %ymm0, (%rdi)
    vzeroupper
    ret

由于我不明白的原因,这实际上隐藏在一个方法中,该方法由实际方法调用,只是在一些内存中移动 - 我不知道为什么Eigen / gcc不会认为参数已经存在正确对齐。如果我使用clang 3.8.0(和相同的参数)编译相同的内容,则编译为:

    vmovaps (%rsi), %ymm0
    vmovaps %ymm0, (%rdi)
    vmovaps 32(%rsi), %ymm0
    vmovaps %ymm0, 32(%rdi)
    vmovaps (%rdi), %ymm1
    vmovaps (%rdx), %ymm2
    vmovaps 32(%rdx), %ymm3
    vmulps  %ymm2, %ymm1, %ymm4
    vmulps  %ymm3, %ymm0, %ymm5
    vsubps  %ymm5, %ymm4, %ymm4
    vmulps  %ymm3, %ymm1, %ymm1
    vmulps  %ymm0, %ymm2, %ymm0
    vaddps  %ymm1, %ymm0, %ymm0
    vmovaps %ymm0, 32(%rdi)
    vmovaps %ymm4, (%rdi)
    movq    %rdi, %rax
    vzeroupper
    retq

同样,开头的记忆运动很奇怪,但至少那是矢量化的。对于gcc和clang,当在循环中调用时,这会被优化掉:

complex8 f8(complex8 x[], int n) {
    if(n==0)
        return complex8(v8sf::Ones(),v8sf::Zero()); // I guess you want p = 1 + 0*i at the beginning?

    complex8 p = x[0];
    for (int i = 1; i < n; i++) p = prod(p, x[i]);
    return p;
}

这里的区别在于clang会将每个循环的外循环展开为2次乘法。另一方面,当使用-mfma编译时,gcc将使用融合乘法加法指令。

f8函数当然也可以推广到任意维度:

template<int size>
complexX<size> fX(complexX<size> x[], int n) {
    using S= typename complexX<size>::value_type;
    if(n==0)
        return complexX<size>(S::Ones(),S::Zero());

    complexX<size> p = x[0];
    for (int i = 1; i < n; i++) p *=x[i];
    return p;
}

为了将complexX<N>缩减为单个std::complex,可以使用以下函数:

// only works for powers of two
template<int size> EIGEN_ALWAYS_INLINE
std::complex<float> redux(const complexX<size>& var) {
    complexX<size/2> a(var.real().template head<size/2>(), var.imag().template head<size/2>());
    complexX<size/2> b(var.real().template tail<size/2>(), var.imag().template tail<size/2>());
    return redux(a*b);
}
template<> EIGEN_ALWAYS_INLINE
std::complex<float> redux(const complexX<1>& var) {
    return std::complex<float>(var.real()[0], var.imag()[0]);
}

但是,根据我是否使用clang或g ++,我得到了完全不同的汇编输出。总的来说,g ++倾向于无法内联加载输入参数,并且clang无法使用FMA操作(YMMV ...) 实质上,您还需要检查生成的汇编代码。更重要的是,您应该对代码进行基准测试(不确定,此例程对您的整体问题有多大影响)。

另外,我想说明Eigen实际上是一个线性代数库。利用纯粹的便携式SIMD代码生成它并不是真正的设计目标。

答案 1 :(得分:3)

如果您主要关心可移植性,那么有许多库here以自己的语法提供SIMD指令。他们中的大多数做的显式矢量化比内在函数更简单和便携。 This Library (UME::SIMD)最近发布,效果很好

  

this paper(UME::SIMD)中,已建立基于Vc的界面   被命名为UME :: SIMD。它允许程序员访问SIMD   功能,无需广泛了解SIMD ISA。   UME :: SIMD提供简单,灵活和可移植的抽象   与...相比没有性能损失的显式矢量化   内在

答案 2 :(得分:1)

我认为你没有完全通用的解决方案。你可以增加你的&#34; vector_size&#34;到32:

typedef float v4sf __attribute__ ((vector_size (32)));

还要将所有数组增加到8个元素:

typedef float v8sf __attribute__ ((vector_size (32)));

typedef union {
  v8sf v;
  float e[8];
} float8;
typedef struct {
  float8 x;
  float8 y;
} complex8;
static complex8 complex8_mul(complex8 a, complex8 b) {
  return (complex8){a.x.v*b.x.v -a.y.v*b.y.v, a.y.v*b.x.v + a.x.v*b.y.v};
}

这将使编译器能够生成AVX512代码(不要忘记添加-mavx512f),但是通过使内存传输次优,会使您的代码在SSE中稍微恶化。但是,它肯定不会禁用SSE矢量化。

你可以保留两个版本(带有4个和8个数组元素),通过一些标志在它们之间切换,但它可能太繁琐而没什么好处。