GCC向量扩展的内存对齐问题

时间:2017-01-20 09:17:18

标签: c++ gcc simd

我正在尝试使用GCC向量扩展(https://gcc.gnu.org/onlinedocs/gcc/Vector-Extensions.html)来加速矩阵乘法。我们的想法是使用SIMD指令一次乘以并添加四个浮点数。下面列出了一个最小的工作示例。当将(M = 10,K = 12)矩阵乘以(K = 12,N = 12)矩阵时,该示例工作正常。但是,当我更改参数(比如N = 9)时,我会遇到分段错误。

我怀疑这是由于内存对齐问题。根据我的理解,当使用SIMD作为16字节的向量(在这种情况下为float4)时,目标存储器地址应该是16的倍数。已经讨论了SIMD指令的存储器对齐问题。 (例如Relationship between SSE vectorization and Memory alignment)。在下面的示例中,当& b(0,0)为0x810e10时,& b(1,0)为0x810e34,这不是16的倍数。

我的问题是,

  1. 我是否因为内存对齐问题而遇到了段错误?
  2. 有谁能告诉我如何轻松解决问题?我曾想过使用二维数组而不是一个数组,但我不想这样做,以免改变其余的代码。
  3. 最小工作示例

    #include <iostream>
    #include <cstdlib>
    #include <stdio.h>
    #include <cstring>
    #include <assert.h>
    #include <algorithm>
    using namespace std;
    typedef float float4 __attribute__((vector_size (16)));
    
    static inline void * alloc64(size_t sz) {
      void * a = 0;
      if (posix_memalign(&a, 64, sz) != 0) {
        perror("posix_memalign");
        exit(1);
      }
      return a;
    }
    
    struct Mat {
        size_t m,n;
        float * a;
        Mat(size_t m_, size_t n_, float f) {
            m = m_;
            n = n_;
            a = (float*) malloc(sizeof(float) * m * n);
            fill(a,a + m * n,f);
        }
      /* a(i,j) */
        float& operator()(long i, long j) {
            return a[i * n + j];
        }
    };
    
    Mat operator* (Mat a, Mat b) {
        Mat c(a.m, b.n,0);
        assert(a.n == b.m);
        for (long i = 0; i < a.m; i++) {
            for(long k = 0; k < a.n; k++){
                float aa = a(i,k);
                float4 a4 = {aa,aa,aa,aa};
                long j;
                for (j = 0; j <= b.n-4; j+=4) {
                    *((float4 *)&c(i,j)) =  *((float4 *)&c(i,j)) + a4 * (*(float4 *)&b(k,j));
                }
                while(j < b.n){
                    c(i,j) += aa * b(k,j);
                    j++;
                }
            }
        }
        return c;
    }
    
    
    const int M = 10;
    const int K = 12;
    const int N = 12;
    
    int main(){
        Mat a(M,K,1);
        Mat b(K,N,1);
        Mat c = a * b;
        for(int i = 0; i < M; i++){
            for(int j = 0; j < N; j++)
                cout << c(i,j) << " ";
            cout << endl;
        }
        cout << endl;
    }
    

1 个答案:

答案 0 :(得分:0)

  

在我的理解中,当使用SIMD用于16字节(in   这种情况下float4),目标内存地址应该是倍数   16。

在x64处理器上这是不正确的。有些指令需要对齐,但您可以很好地从未对齐的存储器位置写入和读取SIMD寄存器而不会受到惩罚,并且使用正确的指令绝对安全。

  

我是否因为内存对齐而遇到了段错误   问题

但它与SIMD指令无关。在C / C ++中,以你的方式编写*((float4 *)&c) = ...是未定义的行为,并且肯定会崩溃,但你可以在没有向量化的情况下重现问题......在适当的情况下,以下基本代码将崩溃。

char * c = ... *(int *) c = 1;

  

有谁能告诉我如何轻松解决问题?我已经想过了   使用二维数组而不是一个数组,但我不想要   这样做是为了不改变其余的代码。

典型的解决方法是使用memcpy。我们来看一个代码示例......

#include <string.h>

typedef float float4 __attribute__((vector_size (16)));

void writeover(float * x, float4 y) {
  *(float4 * ) x = y;
}


void writeover2(float * x, float4 y) {
  memcpy(x,&y,sizeof(y));
}

使用clang ++,这两个函数被编译为vmovapsvmovups。这些是等效的指令,但如果指针未在sizeof(float4)上对齐,则第一个指令会崩溃。它们在最近的硬件上功能非常快。

重点是,您通常可以依靠memcpy生成几乎最快的代码。当然,您获得的开销(如果有的话)将取决于您使用的编译器。

如果您确实遇到了性能问题,那么您可以使用英特尔内在函数或汇编代替......但memcpy很有可能为您提供服务。

另一种解决方法是仅使用float4 *指针。这会强制所有矩阵具有可被4整除的维度,但如果用剩余的填充剩余部分,则可能会得到简单且非常快的代码。