2016-04-13 13 views
4

この質問は、C++最適化手法に関するものです。私は大きな次元で行列 - ベクトル乗算をしており、ランタイムを減らしたいと考えています。私は、線形代数のための特別なライブラリがあることを知っていますが、実際には基礎となるプロセッサの特質について少しは学びたいと思います。これまでは\ O2(Microsoft)でコンパイルしていましたが、コンパイラは乗算の内部ループがベクトル化されていることを確認しました。行列ベクトル乗算の最適化 - キャッシュサイズ

のコード例は次のとおりです。

#include <stdio.h> 
#include <ctime> 
#include <iostream> 

#define VEC_LENGTH 64 
#define ITERATIONS 4000000 

void gen_vector_matrix_multiplication(double *vec_result, double *vec_a, double *matrix_B, unsigned int cols_B, unsigned int rows_B) 
{ 
    // initialise result vector 
    for (unsigned int i = 0; i < rows_B; i++) 
    { 
     vec_result[i] = 0; 
    } 
    // perform multiplication 
    for (unsigned int j = 0; j < cols_B; j++) 
    { 
     const double entry = vec_a[j]; 
     const int col = j*rows_B; 

     for (unsigned int i = 0; i < rows_B; i++) 
     { 
      vec_result[i] += entry * matrix_B[i + col]; 
     } 
    } 
} 

int main() 
{ 
    double *vec_a = new double[VEC_LENGTH]; 
    double *vec_result = new double[VEC_LENGTH]; 
    double *matrix_B = new double[VEC_LENGTH*VEC_LENGTH]; 

    // start clock 
    clock_t begin = clock(); 

    // this outer loop is just for test purposes so that the timing becomes meaningful 
    for (unsigned int i = 0; i < ITERATIONS; i++) 
    { 
     gen_vector_matrix_multiplication(vec_result, vec_a, matrix_B, VEC_LENGTH, VEC_LENGTH); 
    } 

    // stop clock 
    double elapsed_time = static_cast<double>(clock() - begin)/CLOCKS_PER_SEC; 
    std::cout << elapsed_time/(VEC_LENGTH*VEC_LENGTH) << std::endl; 

    delete[] vec_a; 
    delete[] vec_result; 
    delete[] matrix_B; 

    return 1; 
} 

乗算が実行時の信頼性の推定値を得るために数回行われます。私はいくつかの異なるベクトル長さのランタイムを測定しました(この例では、ベクトルの長さであるNという要素が1つしかなく、同時に行列のサイズを定義しますNxN)要素の数に

enter image description here

あなたは十分に小さいNため、操作ごとの実行時間が一定であることがわかります。ただし、N=512を超えると、ランタイムが上に飛びます。青と赤のデータポイントの違いは、プロセッサの負荷です。サンプルプログラムがほとんど単独で実行されている場合は、ランタイムは青い点で、他のコアがビジーのときは赤い点で表されます。

私は今これに関するいくつかの質問があります。私はN=512N=1024間のジャンプは、6メガバイトである必要があり、私のプロセッサ(アイビーブリッジi5-3570)のL3キャッシュのサイズに関係していると仮定して修正

  • アム? 512*512*8byteは約2MB、1024*1024*8byteは約8MBです。そのため、マトリックスがキャッシュにもう収まらないため、RAMからデータをフェッチすることが実行時間が長くなる理由です。
  • 実行時間がこのしきい値を超えて増加し続けている理由は何ですか?
  • ビジー状態とアイドル状態のプロセッサのカーブがしきい値を超えて大きく異なる理由は何ですか?
  • N>1024で操作するためにこの乗算ルーチンを最適化する際の論理的な次のステップは何でしょうか?

私はあなたの考えを聞いて興味があります。ありがとう!

+0

@ tobi303複雑さはありますか?これはベクトル行列の乗算であり、行列行列ではありません。 N * N操作だけがあります。 –

+0

ups、申し訳ありませんが、この情報を欠落している必要があります;) – user463035818

+0

オプティマイザはvector_aとmatrix_bをconst double *として宣言することを推奨します。 Cを使用している場合は、「制限」も使用することをお勧めします。これはC++ではありませんが、gccなどのコンパイラの中には拡張の形式を実装しているものもあります。 – dmuir

答えて

1

は、正規化のために、私は

elapsed_time/(VEC_LENGTH*VEC_LENGTH*ITERATIONS) 

を選んだ、それは6ナノ秒で起動し、Nすべてのケースについて= 8192

ITERATIONS=20 

のみキャッシュされたものにN = 64から7ナノ秒で終了しました"vec_a"なので、大きな行列のために行列要素だけがメモリから読み込まれます。

メモリの帯域幅は約20 GB /秒です。これは、1秒あたり2 G倍を超えることを意味します。 コアの周波数は3.7 GHzなので、これは最大で3.7 Gの乗算になります。

コアでは毎秒3.7Gの倍数を発行できますが、メモリフィードでは秒あたり2Gの意味になります。

もちろん、これは64ビットfp操作のみです。

i + col 

これは乗算の前に実行する必要があるため、これがシリアル実行です。 3.7GHzでの2命令は、効果的にほぼ1.8G /秒を意味します。 2に近い。 キャッシュがその仕事をしても、CPUコアはこのシリアルコードの計算能力に欠けている。

ループを4回展開すると同じことが起こりました。これが半減しました!今では1動作あたり3.4ナノ秒ですが、CPUが必要とする1単位のメモリ帯域幅のあとに2つのインストラクション(1の整数と1の浮動小数点)が存在するため、すべてのNの値になります。

編集:すべてのコアを使用するとメモリ帯域幅を超え、L3キャッシュの効果がより顕著になります。

+1

それは興味深いです。どうもありがとうございました!あなたが反復ごとにほぼ一定の時間を測定するのは不思議です。私はループを4でアンロールしようとしましたが、今度はすべてのNのランタイムもほぼ一定ですが、大規模なNのレベルではほぼ一定です。 –

+0

すべてのコアを使用して過去のRAM帯域幅を取得し、L3キャッシュの利点を発揮してください。 –

+0

ええ、私は内部ループを並列化しようとしましたが、最大のNに対して約20%の改善が得られました。N = 8192未満では、並列化のオーバーヘッドによって実際にランタイムが増加します。 –

2

このようなコードを最適化する重要な点は、エイリアシングとベクトル化を処理していることです。あなたの投稿では、後者をすでに処理していることが示唆されています。ほとんどの場合、コンパイラは少しの助けが必要です。 GCC 5.3.0では、以下のループを使用するとランタイムが大幅に減少します。 __restrict__修飾子は、エイリアシングが可能でないことをコンパイラに通知します。#pragma GCC ivdepは、GCCコンパイラに対して、コードをベクトル化しても問題ないことを通知します。さらに、コンパイラフラグも非常に重要です。 g++ -O3 -march=native -mtune=native matrix_example.cxxを使ってコードをコンパイルしました。

+0

非常にあなたの答えをありがとう!あなたが正しいです、ベクトル化は多くの助けになりました(しかし、質問に表示されているデータはすでにベクトル化ループを使用しています)。エイリアシングのヒントは良かった。私はいくつか興味深いものを読むことができました。しかし、何とかrestrict修飾子を使用しても、私の特定の状況で状況は改善されませんでした。 –

+0

@AlexanderBüse、あなたのコンパイラのための制限修飾子が何であるかを確認してください。 Intelは 'restrict'、gcc、clang' __restrict__'を使用していますが、覚えていればMicrosoftは '__restrict'を使用しています。 – Chiel

+0

はい、あなたはそれが '__restrict'ですが、残念ながらそれはランタイムを改善しませんでした。 –