临时/"非可寻址"固定大小数组?

4
标题取得不够好,而且我不确定我是否已经清楚地表达了自己。我正在寻找一种通过索引访问“数据类型”的方法,但不强制编译器将其保留在数组中。该问题出现在基于SSE/AVX内部函数的低级代码编写中。为了方便编程,我想编写以下代码,使用“寄存器”(数据类型__m512)上的固定长度循环:
inline void load(__m512 *vector, const float *in)
{
    for(int i=0; i<24; i++)
        vector[i] = _mm512_load_ps((in + i*SIMD_WIDTH));
}
// similarely: inline add(...) and inline store(...)

void add(float *in_out, const float *in)
{
    __m512 vector1[24];
    __m512 vector2[24];

    load(vector1, in_out);
    load(vector2, in);
    add(vector1, vector2);
    store(in_out, vector1);
}
< p >由于< code >vector1和vector2被定义为数组,这似乎对编译器(icc在我的情况下)有些麻烦:它似乎被迫使它"可寻址",将其保留在堆栈上,从而生成了许多我不需要的loadstore指令。据我所知,这是为了允许使用vector1vector2进行指针算术运算。

我希望编译器将所有内容都保留在寄存器中。我知道这是可能的:如果我写的代码没有使用__m512数组,而是使用许多单独的__m512变量,编译器会生成更好的代码。

解决方案:

我尝试使用以下类(但没有成功):(我希望switch语句和其他所有内容都会被优化掉):

class Vector
{
    inline __m512 & operator[](int i)
    {
        switch(i)
            case 0:  return component0;
            // ...
            case 23: return component23;
    }

    __m512 component0;
    // ...
    __m512 component23;
};

我也考虑过宏,但找不到好的解决方案。

有什么建议吗?

谢谢,
Simon

以下是在下方回答中的评论,这是一个更详细的示例(虽然仍然简化):

inline void project(__m512 *projected_vector, __m512 *vector)
{
    for(int i=0; i<3; i++)
        projected_vector[i] = _mm512_add_ps(vector[i], vector[i+3]);
}

inline void matrix_multiply(__m512 *out, const float *matrix, __m512 *in)
{
    for(int i=0; i<3; i++)
    {
        out[i] = _mm512_mul_ps(  matrix[3*i+0], in[0]);
        out[i] = _mm512_fmadd_ps(matrix[3*i+1], in[1], out[i]);
        out[i] = _mm512_fmadd_ps(matrix[3*i+2], in[2], out[i]);
    }
}

inline void reconstruct(__m512 *vector, __m512 *projected_vector)
{
    for(int i=0; i<3; i++)
        vector[i] =   _mm512_add_ps(vector[i], projected_vector[i]);
    for(int i=0; i<3; i++)
        vector[i+3] = _mm512_sub_ps(vector[i], projected_vector[i]);
}

inline void hopping_term(float *in_out, const float *matrix_3x3, const float *in)
{
    __m512 vector_in[6];
    __m512 vector_out[6];
    __m512 half_vector1[3];
    __m512 half_vector2[3];

    load(vector_in, in);
    project(half_vector1, vector_in);
    matrix_multiply(half_vector2, matrix_3x3, half_vector1);
    load(vector_out, in_out);
    reconstruct(vector_out, half_vector2);
    store(in_out, vector_out);
}

如果您想走宏路线,并且不想自己做所有的工作,P99可能会有一些有用的东西:http://p99.gforge.inria.fr/p99-html/ - ninjalj
我对SSE/AVX一无所知,但似乎使用代理的自定义向量会很有用。 - Mooing Duck
@Mooing Duck:你能详细解释一下吗?问题中给出的class Vector不是类似于__m512 component0 ... component23的代理吗?我的方法没有带来期望的效果,你会改变什么吗? - Simon
2个回答

1
我最近使用模板元编程做了非常类似的事情。在以下代码中,我认为您只需要将_mm256替换为_mm512并将SIMD_WIDTH更改为16。这应该会展开您的循环24次。
#include <x86intrin.h>

#define SIMD_WIDTH 8
#define LEN 24*SIMD_WIDTH

template<int START, int N>
struct Repeat {
    static void add (float * x, float * y, float * z) {
        _mm256_store_ps(&z[START],_mm256_add_ps(_mm256_load_ps(&x[START]) ,_mm256_load_ps(&y[START])));
        Repeat<START+SIMD_WIDTH, N>::add(x,y,z);
    }
};

template<int N>
    struct Repeat<LEN, N> {
    static void add (float * x, float * y, float * z) {}
};


void sum_unroll(float *x, float *y, float *z, const int n) {
    Repeat<0,LEN>::add(x,y,z);  
}

当我使用g++ -mavx -O3 -S -masm=intel编译时,汇编代码如下:
    vmovaps ymm0, YMMWORD PTR [rdi]
    vaddps  ymm0, ymm0, YMMWORD PTR [rsi]
    vmovaps YMMWORD PTR [rdx], ymm0
    vmovaps ymm0, YMMWORD PTR [rdi+32]
    vaddps  ymm0, ymm0, YMMWORD PTR [rsi+32]
    vmovaps YMMWORD PTR [rdx+32], ymm0
    vmovaps ymm0, YMMWORD PTR [rdi+64]
    vaddps  ymm0, ymm0, YMMWORD PTR [rsi+64]
    vmovaps YMMWORD PTR [rdx+64], ymm0
    vmovaps ymm0, YMMWORD PTR [rdi+96]
    vaddps  ymm0, ymm0, YMMWORD PTR [rsi+96]
    vmovaps YMMWORD PTR [rdx+96], ymm0
    vmovaps ymm0, YMMWORD PTR [rdi+128]
    vaddps  ymm0, ymm0, YMMWORD PTR [rsi+128]
    vmovaps YMMWORD PTR [rdx+128], ymm0
    ...
    vmovaps ymm0, YMMWORD PTR [rdi+736]
    vaddps  ymm0, ymm0, YMMWORD PTR [rsi+736]
    vmovaps YMMWORD PTR [rdx+736], ymm0

我最近成功使用了这种方法,其中我试图一次推送4个微操作(使用聚合更多)。例如,在一个时钟周期内执行两个加载、一个存储和一个二进制浮点数乘加运算。我检查结果以确保没有错误。在我的一些测试中,我能够稍微提高效率。
编辑:根据OP更新的问题,这里有一个解决方案。我过去在使用SIMD变量的数组时也遇到了问题,所以我通常不会与它们一起使用数组。此外,由于寄存器重命名,我很少需要使用许多SIMD寄存器(我认为我使用的最多的是11个)。在这个示例中,只需要五个。
#include <x86intrin.h>

#define SIMD_WIDTH 8

static inline __m256 load(const float *in) { 
    return _mm256_loadu_ps(in);
}

inline void store(float *out, __m256 const &vector) {
    _mm256_storeu_ps(out, vector);
}

inline __m256 project(__m256 const &a, __m256 const &b) {
    return _mm256_add_ps(a, b);
}

inline void reconstruct(__m256 &vector1, __m256 &vector2, __m256 &projected_vector) {
    vector1 = _mm256_add_ps(vector1, projected_vector);
    vector2 = _mm256_sub_ps(vector1, projected_vector); 
}

class So {
public:
    __m256 half_vector[3]; 
    So(const float *in) {
        for(int i=0; i<3; i++) 
            half_vector[i] = project(load(&in[i*SIMD_WIDTH]), load(&in[(3+i)*SIMD_WIDTH]));
    }

    __m256 matrix_multiply(const float *matrix) {
        __m256 out;
        out = _mm256_mul_ps(_mm256_loadu_ps(&matrix[0]), half_vector[0]);
        out = _mm256_fmadd_ps(_mm256_loadu_ps(&matrix[1]), half_vector[1], out);
        out = _mm256_fmadd_ps(_mm256_loadu_ps(&matrix[2]), half_vector[2], out);
        return out;
    }
};

void hopping_term(float *in_out, const float *matrix_3x3, const float *in)
{   

    So so(in);
    for(int i=0; i<3; i++) {
        __m256 vector_out1, vector_out2;
        __m256 half_vector2 = so.matrix_multiply(&matrix_3x3[3*i]); 
        vector_out1 = load(&in_out[i*SIMD_WIDTH]);
        reconstruct(vector_out1, vector_out2, half_vector2);
        store(&in_out[(0+i)*SIMD_WIDTH], vector_out1);
        store(&in_out[(3+i)*SIMD_WIDTH], vector_out2);
    }
}

这只使用了五个AVX寄存器。以下是汇编代码。
    vmovups ymm3, YMMWORD PTR [rdx]
    vmovups ymm2, YMMWORD PTR [rdx+32]
    vaddps  ymm3, ymm3, YMMWORD PTR [rdx+96]
    vmovups ymm0, YMMWORD PTR [rdx+64]
    vaddps  ymm2, ymm2, YMMWORD PTR [rdx+128]
    vaddps  ymm0, ymm0, YMMWORD PTR [rdx+160]
    vmulps  ymm1, ymm3, YMMWORD PTR [rsi]
    vfmadd231ps     ymm1, ymm2, YMMWORD PTR [rsi+4]
    vfmadd231ps     ymm1, ymm0, YMMWORD PTR [rsi+8]
    vaddps  ymm4, ymm1, YMMWORD PTR [rdi]
    vsubps  ymm1, ymm4, ymm1
    vmovups YMMWORD PTR [rdi], ymm4
    vmovups YMMWORD PTR [rdi+96], ymm1
    vmulps  ymm1, ymm3, YMMWORD PTR [rsi+12]
    vfmadd231ps     ymm1, ymm2, YMMWORD PTR [rsi+16]
    vfmadd231ps     ymm1, ymm0, YMMWORD PTR [rsi+20]
    vaddps  ymm4, ymm1, YMMWORD PTR [rdi+32]
    vsubps  ymm1, ymm4, ymm1
    vmovups YMMWORD PTR [rdi+32], ymm4
    vmovups YMMWORD PTR [rdi+128], ymm1
    vmulps  ymm3, ymm3, YMMWORD PTR [rsi+24]
    vfmadd132ps     ymm2, ymm3, YMMWORD PTR [rsi+28]
    vfmadd132ps     ymm0, ymm2, YMMWORD PTR [rsi+32]
    vaddps  ymm1, ymm0, YMMWORD PTR [rdi+64]
    vsubps  ymm0, ymm1, ymm0
    vmovups YMMWORD PTR [rdi+64], ymm1
    vmovups YMMWORD PTR [rdi+160], ymm0

我不确定这是否符合我的要求。也许在示例的简洁性中它被遗漏了。据我所知,这基本上使用递归来避免编写循环,即展开操作。但在我的代码中,展开操作并不是问题:编译器已经做得足够好了。在更复杂的示例中,您如何解决问题,例如执行多个不仅仅是简单的“加法”的操作,例如加载向量、执行3-5条指令、存储向量。请注意,在我的情况下,这3-5条指令可能会混合向量元素。 - Simon
@Simon 顺便说一下,GCC 的 -funroll-loops 只展开了八次。我想要更多。这就是为什么我这样做的原因。GCC 也使用了太多的寄存器。由于寄存器重命名,上面的展开只使用了一个逻辑寄存器。 - Z boson
@Simon,你能否给出一个更具体的例子(在问题中编辑一些代码),说明你想要做什么以及为什么我的方法不起作用。然后我可以尝试找到另一种解决方案。 - Z boson
@Simon,根据您的编辑,我更新了我的答案。我在使用数组作为SIMD变量时也遇到了一些麻烦,所以我通常会避免这样做。我通常会明确列出每个变量。在这种情况下,它起作用了。但主要问题是由于寄存器重命名,您通常不需要那么多寄存器。在您展示的示例中,我只需要五个SIMD寄存器。您的代码请求18个。 - Z boson
@Simon,很抱歉我目前还不知道更好的方法。我喜欢你的问题。希望有人能给出更好的答案,因为这也会对我有所帮助。个人而言,我尽量将计算过程保留在一个函数中,以便不必传递太多变量。 - Z boson
显示剩余4条评论

1
为了回答我的问题,我想出了一种基于模板和宏的解决方案。
  • 一个struct保存了__m512变量的“向量”
  • 使用模板函数访问struct元素
  • 向量索引作为模板参数传递,编译器可以优化访问函数中的switch语句
  • 无法使用模板参数的c循环,因此使用可变宏来模拟循环

例如:

struct RegisterVector
{
    __m512 c0;
    __m512 c1;
    __m512 c2;
    __m512 c3;
    __m512 c4;
    __m512 c5;
};

template <int vector_index> __m512 &element(RegisterVector &vector)
{
    switch(vector_index)
    {
        case  0: return vector.c0;
        case  1: return vector.c1;
        case  2: return vector.c2;
        case  3: return vector.c3;
        case  4: return vector.c4;
        case  5: return vector.c5;
    }
}

#define LOOP3(loop_variable, start, ...) \
    do { \
    { const int loop_variable = start + 0; __VA_ARGS__; } \
    { const int loop_variable = start + 1; __VA_ARGS__; } \
    { const int loop_variable = start + 2; __VA_ARGS__; } \
    } while(0)

// simple usage example
LOOP3(c, 0, _mm512_add_ps(element<2*c+0>(vector1), element<2*c+1>(vector2)));

// more complex example: more than one instruction in the loop, cmul and cfmadd itself are inline functions for a complex mul or fmadd
LOOP3(r, 0,
    cmul  (su3[2*(3*0+r)+0], su3[2*(3*0+r)+1], element<2*0+0>(in), element<2*0+1>(in), element<2*r+0>(out), element<2*r+1>(out));
    cfmadd(su3[2*(3*1+r)+0], su3[2*(3*1+r)+1], element<2*1+0>(in), element<2*1+1>(in), element<2*r+0>(out), element<2*r+1>(out));
    cfmadd(su3[2*(3*2+r)+0], su3[2*(3*2+r)+1], element<2*2+0>(in), element<2*2+1>(in), element<2*r+0>(out), element<2*r+1>(out)));

正如您所见,您可以像通常一样在模板参数中进行整数算术运算,并且循环索引也可以用于访问其他数组。

LOOP 宏可能仍有改进的空间,但对于我的目的来说已经足够了。

Simon


太棒了!我很高兴你找到了适合你的解决方案。我不再擅长C++了。每天我都离C++越来越远,更接近C。我刚开始用NASM编写汇编代码。顺便问一下,你在使用Intel XeonPhi吗?你如何访问AVX512? - Z boson
是的,它是为Xeon Phi设计的。您在我的代码中看到的内部函数是针对Xeon Phi的。它们与AVX512不完全相同,但非常接近,并且通常看起来相同。请参见此处:https://software.intel.com/sites/products/documentation/doclib/iss/2013/compiler/cpp-lin/index.htm#GUID-FC13EE09-8555-414B-8FF2-D7D66CD3975C.htm - Simon

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