【问题标题】:Memory Alignment Issues with GCC Vector ExtensionGCC 向量扩展的内存对齐问题
【发布时间】:2017-01-20 09:17:17
【问题描述】:

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

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

我的问题是,

  1. 我确实遇到了内存对齐问题的段错误吗?
  2. 谁能告诉我如何轻松解决问题?我曾想过使用二维数组而不是一个数组,但我不想这样做,以免更改其余代码。

最小的工作示例

#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;
}

【问题讨论】:

    标签: c++ gcc simd


    【解决方案1】:

    据我了解,当对 16 字节的向量使用 SIMD 时(在 这种情况下 float4),目标内存地址应该是的倍数 16.

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

    我确实得到了内存对齐的段错误 问题?

    是的。

    但它与 SIMD 指令无关。在 C/C++ 中,以您的方式编写 *((float4 *)&amp;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 生成几乎最佳速度的代码。当然,您获得的开销(如果有的话)将取决于您使用的编译器。

    如果您确实遇到了性能问题,那么您可以改用 Intel 内部函数或程序集...但很有可能memcpy 可以很好地为您服务。

    一个不同的解决方法是仅在 float4 * 指针方面工作。这会强制您的所有矩阵具有可被 4 整除的维度,但是如果您用零填充剩余部分,您可能会得到简单且非常快速的代码。

    【讨论】:

    • 非常感谢!我现在太忙了,无法根据您的建议修复我的代码,但我稍后会写一篇后续文章。
    猜你喜欢
    • 1970-01-01
    • 2017-04-13
    • 2013-09-26
    • 1970-01-01
    • 1970-01-01
    • 2011-05-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多