17

Gauss-Seidel法を使用してスパース行列ソルバーを作成しています。プロファイリングにより、プログラムの時間の約半分がソルバー内で費やされていると判断しました。パフォーマンスが重要な部分は次のとおりです。

size_t ic = d_ny + 1, iw = d_ny, ie = d_ny + 2, is = 1, in = 2 * d_ny + 1;
for (size_t y = 1; y < d_ny - 1; ++y) {
    for (size_t x = 1; x < d_nx - 1; ++x) {
        d_x[ic] = d_b[ic]
                - d_w[ic] * d_x[iw] - d_e[ic] * d_x[ie]
                - d_s[ic] * d_x[is] - d_n[ic] * d_x[in];
        ++ic; ++iw; ++ie; ++is; ++in;
    }
    ic += 2; iw += 2; ie += 2; is += 2; in += 2;
}

関連するすべての配列はfloatタイプです。実際には、それらは配列ではなく、オーバーロードされた[]演算子を持つオブジェクトであり、(私は)最適化する必要がありますが、次のように定義されています。

inline float &operator[](size_t i) { return d_cells[i]; }
inline float const &operator[](size_t i) const { return d_cells[i]; }

の場合d_nx = d_ny = 128、これはIntel i7 920で1秒間に約3500回実行できます。これは、内側のループ本体が1秒間に3500 * 128 * 128=5,700万回実行されることを意味します。いくつかの単純な計算しか含まれていないので、それは2.66GHzプロセッサの低い数値として私を驚かせます。

多分それはCPUパワーによってではなく、メモリ帯域幅によって制限されますか?1つの128*128floatアレイは65kBを消費するため、6つのアレイすべてがCPUのL3キャッシュ(8 MB)に簡単に収まるはずです。レジスタに何もキャッシュされていないと仮定すると、内部ループ本体で15回のメモリアクセスをカウントします。64ビットシステムでは、これは反復ごとに120バイトであるため、5,700万*120バイト=6.8GB/秒です。L3キャッシュは2.66GHzで動作するため、同じ桁数です。私の推測では、記憶は確かにボトルネックです。

これをスピードアップするために、私は次のことを試みました:

  • でコンパイルしg++ -O3ます。(まあ、私は最初からこれをやっていた。)

  • OpenMPプラグマを使用した4コア以上の並列化。同じ配列からの読み取りと同じ配列への書き込みを回避するために、Jacobiアルゴリズムに変更する必要があります。これには、2倍の反復を行う必要があり、ほぼ同じ速度の最終結果が得られます。

  • インデックスの代わりにポインタを使用するなど、ループ本体の実装の詳細をいじる。無効。

この男をスピードアップするための最良のアプローチは何ですか?アセンブリで内部ボディを書き直すのに役立ちますか(最初にそれを学ぶ必要があります)?代わりにこれをGPUで実行する必要があります(これは方法を知っていますが、とても面倒です)?他に明るいアイデアはありますか?

(NB私答えに「いいえ」を取ります:「それは大幅に速くすることはできません、なぜなら...」)

更新:要求に応じて、完全なプログラムは次のとおりです。

#include <iostream>
#include <cstdlib>
#include <cstring>

using namespace std;

size_t d_nx = 128, d_ny = 128;
float *d_x, *d_b, *d_w, *d_e, *d_s, *d_n;

void step() {
    size_t ic = d_ny + 1, iw = d_ny, ie = d_ny + 2, is = 1, in = 2 * d_ny + 1;
    for (size_t y = 1; y < d_ny - 1; ++y) {
        for (size_t x = 1; x < d_nx - 1; ++x) {
            d_x[ic] = d_b[ic]
                - d_w[ic] * d_x[iw] - d_e[ic] * d_x[ie]
                - d_s[ic] * d_x[is] - d_n[ic] * d_x[in];
            ++ic; ++iw; ++ie; ++is; ++in;
        }
        ic += 2; iw += 2; ie += 2; is += 2; in += 2;
    }
}

void solve(size_t iters) {
    for (size_t i = 0; i < iters; ++i) {
        step();
    }
}

void clear(float *a) {
    memset(a, 0, d_nx * d_ny * sizeof(float));
}

int main(int argc, char **argv) {
    size_t n = d_nx * d_ny;
    d_x = new float[n]; clear(d_x);
    d_b = new float[n]; clear(d_b);
    d_w = new float[n]; clear(d_w);
    d_e = new float[n]; clear(d_e);
    d_s = new float[n]; clear(d_s);
    d_n = new float[n]; clear(d_n);
    solve(atoi(argv[1]));
    cout << d_x[0] << endl; // prevent the thing from being optimized away
}

私はそれを次のようにコンパイルして実行します:

$ g++ -o gstest -O3 gstest.cpp
$ time ./gstest 8000
0

real    0m1.052s
user    0m1.050s
sys     0m0.010s

(私の「実際の」プログラムは他の多くのことも行うので、毎秒3500回の反復ではなく8000回実行します。しかしそれは代表的なものです。)

アップデート2:NaNとInfの値が遅くなる可能性があるため、単一化された値は代表的ではない可能性があると言われました。次に、サンプルコードのメモリをクリアします。ただし、実行速度には違いはありません。

4

7 に答える 7

5

いくつかのアイデア:

  1. SIMDを使用します。各アレイから一度に4つのフロートをSIMDレジスタにロードできます(たとえば、IntelのSSE、PowerPCのVMX)。これの欠点は、一部のd_x値が「古くなっている」ため、収束率が低下することです(ただし、ヤコビ反復ほど悪くはありません)。スピードアップがそれを相殺するかどうかを言うのは難しいです。

  2. SORを使用します。これは単純で、多くの計算を追加せず、比較的控えめな緩和値(たとえば、1.5)であっても、収束率を非常によく向上させることができます。

  3. 共役勾配法を使用します。これが流体シミュレーションの投影ステップ(つまり、非圧縮性を適用する)の場合は、CGを適用して、はるかに優れた収束率を得ることができるはずです。優れた前提条件はさらに役立ちます。

  4. 専用のソルバーを使用してください。線形連立方程式がポアソン方程式から生じる場合、FFTベースの方法を使用して共役勾配法よりも優れた方法を実行できます。

あなたが解決しようとしているシステムがどのように見えるかについてもっと説明できれば、私はおそらく#3と#4についてもう少しアドバイスを与えることができます。

于 2010-03-06T00:46:34.957 に答える
5

私はそれを最適化することができたと思います。ここにコードがあり、VC ++で新しいプロジェクトを作成し、このコードを追加して、「リリース」でコンパイルするだけです。

#include <iostream>
#include <cstdlib>
#include <cstring>

#define _WIN32_WINNT 0x0400
#define WIN32_LEAN_AND_MEAN
#include <windows.h>

#include <conio.h>

using namespace std;

size_t d_nx = 128, d_ny = 128;
float *d_x, *d_b, *d_w, *d_e, *d_s, *d_n;

void step_original() {
    size_t ic = d_ny + 1, iw = d_ny, ie = d_ny + 2, is = 1, in = 2 * d_ny + 1;
    for (size_t y = 1; y < d_ny - 1; ++y) {
        for (size_t x = 1; x < d_nx - 1; ++x) {
            d_x[ic] = d_b[ic]
                - d_w[ic] * d_x[iw] - d_e[ic] * d_x[ie]
                - d_s[ic] * d_x[is] - d_n[ic] * d_x[in];
            ++ic; ++iw; ++ie; ++is; ++in;
        }
        ic += 2; iw += 2; ie += 2; is += 2; in += 2;
    }
}
void step_new() {
    //size_t ic = d_ny + 1, iw = d_ny, ie = d_ny + 2, is = 1, in = 2 * d_ny + 1;
    float
        *d_b_ic,
        *d_w_ic,
        *d_e_ic,
        *d_x_ic,
        *d_x_iw,
        *d_x_ie,
        *d_x_is,
        *d_x_in,
        *d_n_ic,
        *d_s_ic;

    d_b_ic = d_b;
    d_w_ic = d_w;
    d_e_ic = d_e;
    d_x_ic = d_x;
    d_x_iw = d_x;
    d_x_ie = d_x;
    d_x_is = d_x;
    d_x_in = d_x;
    d_n_ic = d_n;
    d_s_ic = d_s;

    for (size_t y = 1; y < d_ny - 1; ++y)
    {
        for (size_t x = 1; x < d_nx - 1; ++x)
        {
            /*d_x[ic] = d_b[ic]
                - d_w[ic] * d_x[iw] - d_e[ic] * d_x[ie]
                - d_s[ic] * d_x[is] - d_n[ic] * d_x[in];*/
            *d_x_ic = *d_b_ic
                - *d_w_ic * *d_x_iw - *d_e_ic * *d_x_ie
                - *d_s_ic * *d_x_is - *d_n_ic * *d_x_in;
            //++ic; ++iw; ++ie; ++is; ++in;
            d_b_ic++;
            d_w_ic++;
            d_e_ic++;
            d_x_ic++;
            d_x_iw++;
            d_x_ie++;
            d_x_is++;
            d_x_in++;
            d_n_ic++;
            d_s_ic++;
        }
        //ic += 2; iw += 2; ie += 2; is += 2; in += 2;
        d_b_ic += 2;
        d_w_ic += 2;
        d_e_ic += 2;
        d_x_ic += 2;
        d_x_iw += 2;
        d_x_ie += 2;
        d_x_is += 2;
        d_x_in += 2;
        d_n_ic += 2;
        d_s_ic += 2;
    }
}

void solve_original(size_t iters) {
    for (size_t i = 0; i < iters; ++i) {
        step_original();
    }
}
void solve_new(size_t iters) {
    for (size_t i = 0; i < iters; ++i) {
        step_new();
    }
}

void clear(float *a) {
    memset(a, 0, d_nx * d_ny * sizeof(float));
}

int main(int argc, char **argv) {
    size_t n = d_nx * d_ny;
    d_x = new float[n]; clear(d_x);
    d_b = new float[n]; clear(d_b);
    d_w = new float[n]; clear(d_w);
    d_e = new float[n]; clear(d_e);
    d_s = new float[n]; clear(d_s);
    d_n = new float[n]; clear(d_n);

    if(argc < 3)
        printf("app.exe (x)iters (o/n)algo\n");

    bool bOriginalStep = (argv[2][0] == 'o');
    size_t iters = atoi(argv[1]);

    /*printf("Press any key to start!");
    _getch();
    printf(" Running speed test..\n");*/

    __int64 freq, start, end, diff;
    if(!::QueryPerformanceFrequency((LARGE_INTEGER*)&freq))
        throw "Not supported!";
    freq /= 1000000; // microseconds!
    {
        ::QueryPerformanceCounter((LARGE_INTEGER*)&start);
        if(bOriginalStep)
            solve_original(iters);
        else
            solve_new(iters);
        ::QueryPerformanceCounter((LARGE_INTEGER*)&end);
        diff = (end - start) / freq;
    }
    printf("Speed (%s)\t\t: %u\n", (bOriginalStep ? "original" : "new"), diff);
    //_getch();


    //cout << d_x[0] << endl; // prevent the thing from being optimized away
}

次のように実行します。

app.exe 10000 o

app.exe 10000 n

「o」はあなたの古いコードを意味します。

「n」は私のもの、新しいものです。

私の結果:速度(元):

1515028

1523171

1495988

速度(新規):

966012

984110

1006045

約30%の改善。

背後にあるロジック:アクセス/操作にインデックスカウンターを使用しています。ポインタを使用します。実行中に、VC ++のデバッガーの特定の計算コード行でブレークポイントを設定し、F8キーを押します。逆アセンブラウィンドウが表示されます。生成されたオペコード(アセンブリコード)が表示されます。

とにかく、見てください:

int * x = ...;

x [3] = 123;

これは、PCにポインタxをレジスタ(たとえばEAX)に置くように指示します。それを追加します(3 * sizeof(int))。その場合にのみ、値を123に設定します。

ポインタのアプローチは、理解できるようにはるかに優れています。これは、追加プロセスを削減し、実際には自分で処理するため、必要に応じて最適化できるためです。

これがお役に立てば幸いです。

stackoverflow.comのスタッフへの補足:すばらしいウェブサイトです。ずっと前に聞いたことがあると思います。

于 2010-03-05T17:58:22.397 に答える
3

一つには、ここにパイプラインの問題があるようです。ループは、d_x書き込まれたばかりの値から読み取りますが、明らかに、その書き込みが完了するまで待機する必要があります。計算の順序を並べ替えて、待機中に何か便利なことを行うだけで、ほぼ2倍の速度になります。

d_x[ic] = d_b[ic]
                        - d_e[ic] * d_x[ie]
    - d_s[ic] * d_x[is] - d_n[ic] * d_x[in]
    - d_w[ic] * d_x[iw] /* d_x[iw] has just been written to, process this last */;

これを理解したのはEamonNerbonneでした。彼に多くの賛成票が!私は決して推測しなかったでしょう。

于 2010-03-05T20:24:43.643 に答える
2

ポニの答えは私には正しいもののように見えます。

この種の問題では、メモリの局所性から利益を得ることがよくあることを指摘したいと思います。現在、b,w,e,s,nアレイはすべてメモリ内の別々の場所にあります。問題をL3キャッシュ(主にL2)に収めることができなかった場合、これは悪いことであり、この種の解決策が役立ちます。

size_t d_nx = 128, d_ny = 128;
float *d_x;

struct D { float b,w,e,s,n; };
D *d;

void step() {
    size_t ic = d_ny + 1, iw = d_ny, ie = d_ny + 2, is = 1, in = 2 * d_ny + 1;
    for (size_t y = 1; y < d_ny - 1; ++y) {
        for (size_t x = 1; x < d_nx - 1; ++x) {
            d_x[ic] = d[ic].b
                - d[ic].w * d_x[iw] - d[ic].e * d_x[ie]
                - d[ic].s * d_x[is] - d[ic].n * d_x[in];
            ++ic; ++iw; ++ie; ++is; ++in;
        }
        ic += 2; iw += 2; ie += 2; is += 2; in += 2;
    }
}
void solve(size_t iters) { for (size_t i = 0; i < iters; ++i) step(); }
void clear(float *a) { memset(a, 0, d_nx * d_ny * sizeof(float)); }

int main(int argc, char **argv) {
    size_t n = d_nx * d_ny;
    d_x = new float[n]; clear(d_x);
    d = new D[n]; memset(d,0,n * sizeof(D));
    solve(atoi(argv[1]));
    cout << d_x[0] << endl; // prevent the thing from being optimized away
}

たとえば、1280x1280でのこのソリューションはPoniのソリューションよりも2倍弱高速です(私のテストでは13秒対23秒-元の実装は22秒です)が、128x128では30%遅くなります(7秒対10秒-元のは10秒です)。

(反復は、ベースケースでは80000に、1280x1280の100倍大きいケースでは800にスケールアップされました。)

于 2010-03-05T20:41:09.580 に答える
1

私はこのテーマの専門家ではありませんが、ガウス・ザイデル法のキャッシュ使用法の改善に関するいくつかの学術論文があることを確認しました。

もう1つの可能な最適化は、赤と黒のバリアントの使用です。この場合、ポイントはチェス盤のようなパターンで2回のスイープで更新されます。このようにして、スイープ内のすべての更新は独立しており、並列化できます。

于 2010-03-05T20:50:04.673 に答える
1

記憶がボトルネックになっているのは正しいと思います。これは非常に単純なループであり、反復ごとにいくつかの単純な演算が行われます。ic、iw、つまりis、およびinインデックスはマトリックスの反対側にあるように見えるので、そこにはたくさんのキャッシュミスがあると思います。

于 2010-03-05T16:59:58.047 に答える
0

プリフェッチステートメントをいくつか入れて、「データ指向の設計」についても調査することをお勧めします。

void step_original() {
    size_t ic = d_ny + 1, iw = d_ny, ie = d_ny + 2, is = 1, in = 2 * d_ny + 1;
    float dw_ic, dx_ic, db_ic, de_ic, dn_ic, ds_ic;
    float dx_iw, dx_is, dx_ie, dx_in, de_ic, db_ic;
    for (size_t y = 1; y < d_ny - 1; ++y) {
        for (size_t x = 1; x < d_nx - 1; ++x) {
// Perform the prefetch
// Sorting these statements by array may increase speed;
//    although sorting by index name may increase speed too.
            db_ic = d_b[ic];
            dw_ic = d_w[ic];
            dx_iw = d_x[iw];
            de_ic = d_e[ic];
            dx_ie = d_x[ie];
            ds_ic = d_s[ic];
            dx_is = d_x[is];
            dn_ic = d_n[ic];
            dx_in = d_x[in];
// Calculate
            d_x[ic] = db_ic
                - dw_ic * dx_iw - de_ic * dx_ie
                - ds_ic * dx_is - dn_ic * dx_in;
            ++ic; ++iw; ++ie; ++is; ++in;
        }
        ic += 2; iw += 2; ie += 2; is += 2; in += 2;
    }
}

計算が実行される前に値がローカル一時変数にコピーされるため、これは2番目の方法とは異なります。

于 2010-03-05T22:54:06.800 に答える