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

Posted

技术标签:

【中文标题】GCC 向量扩展的内存对齐问题【英文标题】:Memory Alignment Issues with GCC Vector Extension 【发布时间】: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 的倍数。

我的问题是,

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

最小的工作示例

#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】:

据我了解,当对 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 整除的维度,但是如果您用零填充剩余部分,您可能会得到简单且非常快速的代码。

【讨论】:

非常感谢!我现在太忙了,无法根据您的建议修复我的代码,但我稍后会写一篇后续文章。

以上是关于GCC 向量扩展的内存对齐问题的主要内容,如果未能解决你的问题,请参考以下文章

gcc数据对齐之: howto 1.

gcc 结构中的内存对齐

struct内存对齐1:gcc与VC的差别

犰狳向量 vec/fvec 的内存对齐

C语言之gcc中支持的内存对齐指令

如何使用 Altivec 将向量存储到内存中未对齐的位置