2017-01-20 30 views
0

行列の乗算を高速化するために、GCCベクター拡張子(https://gcc.gnu.org/onlinedocs/gcc/Vector-Extensions.html)を使用しようとしています。この考え方は、SIMD命令を使用して4つの浮動小数点数を一度に増やして加算することです。最小限の作業例を以下に示します。この例は、(M = 10、K = 12)行列に(K = 12、N = 12)行列を乗算するとうまく動作します。しかし、パラメータを変更すると(N = 9など)、セグメント化エラーが発生します。GCCベクター拡張でのメモリーアライメントの問題

これはメモリアライメントの問題によるものだと思われます。私の理解では、16バイト(この場合はfloat4)のベクトルにSIMDを使用する場合、ターゲットメモリアドレスは16の倍数でなければなりません。SIMD命令によるメモリアラインメントの問題については既に議論があります。 (例えば、Relationship between SSE vectorization and Memory alignment)。以下の例では、& B(0,0)0x810e10あるとき、& B(1,0)は、私の質問がある16

の倍数ではありません。これは、0x810e34ある

  1. がそれです私がメモリアライメントの問題のセグメンテーションを取得しているのは本当ですか?
  2. 問題を簡単に解決する方法を教えてもらえますか?私は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; 
} 

答えて

0

(この場合のfloat4が に)ベクトルWICHの16バイトのためのSIMDを使用する場合、ターゲット・メモリ・アドレスは 16の倍数でなければなりません

x64プロセッサでは正しくありません。アライメントを必要とする命令がありますが、ペナルティなしで正しい命令を使用して安全性を確保しながら、アライメントされていないメモリ位置からSIMDレジスタを完全に書き込み、読み取ることができます。

メモリアライメントのセグメンテーションを取得していることは本当ですか? ?

はい。

しかし、SIMD命令とは関係ありません。 C/C++では、*((float4 *)&c) = ...と書くと未定義の動作ですが、クラッシュする可能性はありますが、ベクトル化せずに問題を再現することができます...

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

問題を簡単に解決する方法を教えていただけますか?私は の1つの配列の代わりに2次元配列を使用して考えましたが、私は が残りのコードを変更しないようにしたくありません。

一般的な回避策は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)); 
} 

をしてみましょう、この2つの関数はvmovapsvmovupsにコンパイルさ。これらは同等の指示ですが、ポインタがsizeof(float4)に位置合わせされていないと、最初のものがクラッシュします。最近のハードウェアでは非常に高速な機能です。

ほとんどの場合、ほぼ最適に高速なコードを生成するために、memcpyを使用することができます。もちろん、(もしあれば)オーバーヘッドの量は、使用しているコンパイラに依存します。

パフォーマンスの問題が発生した場合は、代わりにインテル®インテリジェンスまたはアセンブリを使用できますが、memcpyがうまく機能する可能性があります。

別の修正プログラムは、ポインタfloat4 *の点でのみ動作します。これにより、すべての行列の次元が4で割り切れるようになりますが、残りの部分をゼロで埋めると、シンプルで素早いコードが得られるでしょう。

+0

ありがとうございました!あなたの提案に基づいて私のコードを修正するのは余りにも忙しいですが、私はあとでフォローアップ投稿を書くつもりです。 – user3127171