1

次元が非常に大きいと仮定します(マトリックス内の最大10億個の要素)。行列-ベクトル積のキャッシュ忘却アルゴリズムをどのように実装しますか?ウィキペディアに基づいて、再帰的に分割統治する必要がありますが、多くのオーバーヘッドがあるように感じます。そうすることは効率的でしょうか?

質問と回答のフォローアップ:行列とベクトルを使用したOpenMP

4

1 に答える 1

3

したがって、「この基本的な線形代数演算を高速化するにはどうすればよいか」という質問に対する答えは、いつでもどこでも、プラットフォーム用に調整された BLAS ライブラリを見つけてリンクすることです。たとえば、GotoBLAS (その作業はOpenBLASで継続されています)、低速の自動調整されたATLAS、または Intel の MKL のような商用パッケージです。線形代数は、他の多くの演算にとって非常に基本的なものであるため、これらのパッケージをさまざまなプラットフォーム用に最適化するために膨大な労力が費やされており、午後の数回の作業で競合するものを思いつく可能性はまったくありません. 一般的な密な行列とベクトルの乗算のために探している特定のサブルーチン呼び出しは、SGEMV/DGEMV/CGEMV/ZGEMV です。

キャッシュを無視するアルゴリズム、または自動チューニングは、システムの特定のキャッシュ アーキテクチャのチューニングに煩わ​​されない場合に使用します。調整された結果が利用可能であるということは、それらのルーチンを使用することだけが最善であることを意味します。

GEMV のメモリ アクセス パターンは単純なので、実際に分割統治を行う必要はありません (行列転置の標準的なケースと同じです)。キャッシュのブロック サイズを見つけて使用するだけです。GEMV (y = Ax) では、マトリックス全体を 1 回スキャンする必要があるため、そこで再利用する (したがってキャッシュを効果的に使用する) ために何もする必要はありませんが、x をできるだけ再利用してロードすることができます。 (行数) 回ではなく 1 回 - それでも A へのアクセスをキャッシュに適したものにしたい。したがって、明らかにキャッシュをブロックすることは、ブロックに沿って分割することです。

  A x -> [ A11 | A12 ] | x1 | = | A11 x1 + A12 x2 |
         [ A21 | A22 ] | x2 |   | A21 x1 + A22 x2 |

そして、あなたは確かにそれを再帰的に行うことができます。しかし、素朴な実装を行うと、単純な二重ループよりも遅く、適切な SGEMV ライブラリ呼び出しよりもはるかに遅くなります。

$ ./gemv
Testing for N=4096
Double Loop: time = 0.024995, error = 0.000000
Divide and conquer: time = 0.299945, error = 0.000000
SGEMV: time = 0.013998, error = 0.000000

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

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include "mkl.h"

float **alloc2d(int n, int m) {
    float *data = malloc(n*m*sizeof(float));
    float **array = malloc(n*sizeof(float *));
    for (int i=0; i<n; i++)
        array[i] = &(data[i*m]);
    return array;
}

void tick(struct timeval *t) {
    gettimeofday(t, NULL);
}

/* returns time in seconds from now to time described by t */
double tock(struct timeval *t) {
    struct timeval now;
    gettimeofday(&now, NULL);
    return (double)(now.tv_sec - t->tv_sec) + ((double)(now.tv_usec - t->tv_usec)/1000000.);
}

float checkans(float *y, int n) {
    float err = 0.;
    for (int i=0; i<n; i++)
        err += (y[i] - 1.*i)*(y[i] - 1.*i);
    return err;
}

/* assume square matrix */
void divConquerGEMV(float **a, float *x, float *y, int n,
                    int startr, int endr, int startc, int endc) {

    int nr = endr - startr + 1;
    int nc = endc - startc + 1;

    if (nr == 1 && nc == 1) {
        y[startc] += a[startr][startc] * x[startr];
    } else {
        int midr = (endr + startr+1)/2;
        int midc = (endc + startc+1)/2;
        divConquerGEMV(a, x, y, n, startr, midr-1, startc, midc-1);
        divConquerGEMV(a, x, y, n, midr,   endr,   startc, midc-1);
        divConquerGEMV(a, x, y, n, startr, midr-1, midc,   endc);
        divConquerGEMV(a, x, y, n, midr,   endr,   midc,   endc);
    }
}
int main(int argc, char **argv) {
    const int n=4096;
    float **a = alloc2d(n,n);
    float *x  = malloc(n*sizeof(float));
    float *y  = malloc(n*sizeof(float));
    struct timeval clock;
    double eltime;

    printf("Testing for N=%d\n", n);

    for (int i=0; i<n; i++) {
        x[i] = 1.*i;
        for (int j=0; j<n; j++)
            a[i][j] = 0.;
        a[i][i] = 1.;
    }

    /* naive double loop */
    tick(&clock);
    for (int i=0; i<n; i++) {
        y[i] = 0.;
        for (int j=0; j<n; j++) {
            y[i] += a[i][j]*x[j];
        }
    }
    eltime = tock(&clock);
    printf("Double Loop: time = %lf, error = %f\n", eltime, checkans(y,n));

    for (int i=0; i<n; i++) y[i] = 0.;

    /* naive divide and conquer */
    tick(&clock);
    divConquerGEMV(a, x, y, n, 0, n-1, 0, n-1);
    eltime = tock(&clock);
    printf("Divide and conquer: time = %lf, error = %f\n", eltime, checkans(y,n));

    /* decent GEMV implementation */
    tick(&clock);

    float alpha = 1.;
    float beta =  0.;
    int incrx=1;
    int incry=1;
    char trans='N';

    sgemv(&trans,&n,&n,&alpha,&(a[0][0]),&n,x,&incrx,&beta,y,&incry);
    eltime = tock(&clock);
    printf("SGEMV: time = %lf, error = %f\n", eltime, checkans(y,n));

    return 0;
}
于 2012-03-28T12:42:55.517 に答える