2016-12-10 19 views
0

私はAVXでプログラミングすることを学んでいます。そこで、私はサイズ4の行列を乗算する簡単なプログラムを書いた。コンパイラの最適化がないうちに、AVXバージョンは非AVXバージョンよりわずかに高速で、O3最適化では非AVXバージョンはAVXバージョン。どのようにAVXバージョンのパフォーマンスを向上させることができますか?以下は完全なコードです。AVX2でより遅く実行する行列乗算コード

#include <immintrin.h> 
#include <stdio.h>  
#include <stdlib.h>  

#define MAT_SIZE 4 
#define USE_AVX 

double A[MAT_SIZE][MAT_SIZE]; 
double B[MAT_SIZE][MAT_SIZE]; 
double C[MAT_SIZE][MAT_SIZE]; 

union { 
    double m[4][4]; 
    __m256d row[4]; 
} matB; 

void init_matrices() 
{ 
    for(int i = 0; i < MAT_SIZE; i++) 
     for(int j = 0; j < MAT_SIZE; j++) 
     { 
      A[i][j] = (float)(i+j); 
      B[i][j] = (float)(i+j+1); 
      matB.m[i][j] = B[i][j]; 
     } 
} 

void print_result() 
{ 
    for(int i = 0; i < MAT_SIZE; i++) 
    { 
     for(int j = 0; j < MAT_SIZE; j++) 
     { 
      printf("%.1f\t", C[i][j]); 
     } 
     printf("\n"); 
    } 
} 

void withoutAVX() 
{ 
    for(int row = 0; row < MAT_SIZE; row++) 
     for(int col = 0; col < MAT_SIZE; col++) 
     { 
      float sum = 0; 
      for(int e = 0; e < MAT_SIZE; e++) 
       sum += A[row][e] * B[e][col]; 
      C[row][col] = sum; 
     } 
} 

void withAVX() 
{ 
    for(int row = 0; row < 4; row++) 
    { 
     //calculate_resultant_row(row); 
     const double* rowA = (const double*)&A[row]; 
     __m256d* pr = (__m256d*)(&C[row]); 

     *pr = _mm256_mul_pd(_mm256_broadcast_sd(&rowA[0]), matB.row[0]); 
     for(int i = 1; i < 4; i++) 
      *pr = _mm256_add_pd(*pr, _mm256_mul_pd(_mm256_broadcast_sd(&rowA[i]), 
       matB.row[i])); 
    } 
} 

static __inline__ unsigned long long rdtsc(void) 
{ 
    unsigned hi, lo; 
    __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi)); 
    return ((unsigned long long)lo)|(((unsigned long long)hi)<<32); 
} 

int main() 
{ 
    init_matrices(); 

    // start timer 
    unsigned long long cycles = rdtsc(); 
#ifdef USE_AVX 
    withAVX(); 
#else 
    withoutAVX(); 
#endif 
    // stop timer 
    cycles = rdtsc() - cycles; 

    printf("\nTotal time elapsed : %ld\n\n", cycles); 
    print_result(); 
    return 0; 
} 
+1

matmulの繰り返しを複数回行う必要がありますので、正確に測定するのに十分な時間がかかります。 'perf stat'を使ってパフォーマンスカウンタでプログラム全体の時間を計るので、CPU周波数の変更(省電力+ターボ)を心配する必要はありません。 –

+1

また、 'cpuid; matmulが実際に実行される前にRDTSCを実行することからアウト・オブ・オーダーの実行を防ぐために、RDTSC用のパイプラインをシリアル化するために「rdtsc」を使用します。 –

+1

また、 '-O0 'での時刻は特に有用ではないことに注意してください。 gccは '-O3'でのみ自動ベクトル化するので、' -O2'でチェックすることは意味があります –

答えて

3

使用しているコンパイラとシステムを正確に把握していないと確信できません。生成されたコードのアセンブリを確認して確認する必要があります。以下はいくつかの理由によるものです。

コンパイラがおそらく追加のロード/ストアを生成しました。これは費用がかかります。

Aからの最も内側のループブロードキャスト要素です。したがって、余分な負荷があります。最適なコードは、AとBのそれぞれ4つ、Cの4つのストアバックで8つの負荷しか必要としません。しかし、あなたのコードはブロードキャストの使用のために少なくとも16の余分な負荷につながります。これらは計算そのものと同じくらいコストがかかります。

編集(コメントが長すぎます)

コンパイラは、スマートな最適化を行うことはできませんか、いつかそれが良いため、「あまりにも巧妙」である状況があります。最近、アセンブリを使用してコンパイラの最適化を避ける必要がありましたが、実際には悪いコードにつながります。それは、あなたが必要とするものがパフォーマンスなのであれば、実際にどのようにそこに着くかは気にしません。私は最初に良いライブラリを探すことをお勧めします。たとえば、Eigen for linear algebraは、この例ではあなたの必要性に完全に合っています。 SIMDプログラミングを学びたい場合は、2つのベクトルを追加するなどの簡単な場合から始めてください。おそらく、コンパイラは最初のいくつかの試みよりも優れたベクトル化されたバイナリを生成できることがわかります。しかし、彼らはより簡単ですので、より簡単に改善が必要な場所を知ることができます。あなたは、コンパイラが生成できるものと同じかそれ以上のコードを生成しようとする過程で、最適なコードを書くために必要なすべてのものを学びます。コンパイラが最適化できないコードに最適な実装を提供できるようになります。あなたが覚えておく必要があることの1つは、低いレベルの方がコンパイラが少ないことです。生成されるバイナリをより詳細に制御できますが、最適化することも自分の責任です。これらのアドバイスはかなり曖昧です。申し訳ありませんより多くのヘルプのことはできません。

+0

これらの余分な負荷をどのように避けることができますか? _mm256_set1_pdはどうですか? – pythonic

+1

@pythonic正直言って、小さな行列乗算の最適な実装は、実際には非常に困難です。あなたはアイゲンのソースを見て、それがどのようにそれをするか見ることができます。私の頭の中にある考え方は、関数の入力時に、データの格納方法に応じて、行単位または列単位でAとBから8つのロードを実行するということです。そして、解凍の指示でそれらの1つを転置することができます。そして乗算を行う。水平方向の追加はhaddを使って行うことができます。最後にC行列に4つのストアを行います。しかし、多くの実験をせずにこれが良いか悪くなるかは言うまでもありません。 –

+0

私は参照してください。しかし私はこのコード(https://gist.github.com/rygorous/4172889)を見ています。これは私のものとよく似ており、元のコードよりも大幅に改善されています。その奇妙な、それではないですか?また、私が使用しているコンパイラはgcc 6.1です。たぶん、それは非常に速い理由は、コード上で超最適化を行います!コンパイラーがよりスマートでスマートになるにつれて、AVXプログラミングも学習価値がありますか?または、GPUの学習に切り替えるだけでいいですか?:) – pythonic