行列とベクトルに関する計算: Sample 1: 行列と行列の掛け算(C)

高速化プログラミング   
トップ  >  計算量のオーダー  >  行列とベクトルに関する計算  >  Sample 1: 行列と行列の掛け算(C)

行列とベクトルに関する計算: Sample 1: 行列と行列の掛け算(C)

言語の変更:   FORTRAN版

■ 概要

行列と行列の掛け算は、特殊な場合を除いて、計算量のオーダーがO(N3)と決まっています。ここではある特殊な場合を考えます。つまり行列と行列を掛け算したあとにその結果の行列はまたベクトルを掛け算したい場合です。

式で書くと式(1)になります。この計算に必要なのはベクトルyだけであり、行列Cyを計算するためだけであり、yが求まったらCは不要とします。

C=AB; y=Cx           ... 式(1)

式(1)を素直にコーディングしたのはCode 1になります。式(1)の前半はオーダーがO(N3)の行列と行列の掛け算、後半はオーダーがO(N2)の行列とベクトルの掛け算。 全体のオーダーは最大のオーダーで決まるので、O(N3)。

さて式(1)をまとめて書くと、y=(AB)xとなりますが、行列・ベクトルの掛け算の順番を入れ替えることはできるので、式(1)はy=A(Bx)とも書けます。さらに2つの式に分けると式(2)になります。

z=Bx; y=Az           ... 式(2)

式(2)でご注意していただきたいのは行列と行列の掛け算がなくなることです。その代わりに2つの行列とベクトルの掛け算がありますが、行列とベクトルの掛け算はオーダーがO(N2)なので、式(2)のオーダーは式(1)より一つ低く、O(N2)です。

この次にCode 1Code 2を実際に実行するときにどのぐらい計算時間の差が出るかを見てみましょう。

■ ソースコード



  ◆ Code 1   ◆ Code 2
 
#include <stdio.h>
#include <time.h>
int main()
{
  int i, j, k;
  printf("Matrix size    Elapsed time [sec] \n");

  for (int m=1; m<=20; m++) {
    // Matrix size
    int n = m*100;

    // Allocation
    double **a, **b, **c, *x, *y, *dum;
    dum = new double[n*n];
    a = new double*[n];
    for (i=0; i<n; i++)
      a[i] = dum + i*n;

    dum = new double[n*n];
    b = new double*[n];
    for (i=0; i<n; i++)
      b[i] = dum + i*n;

    dum = new double[n*n];
    c = new double*[n];
    for (i=0; i<n; i++)
      c[i] = dum + i*n;

    x = new double[n];
    y = new double[n];


    // Initialization
    for (i=0; i<n; i++) {
      for (j=0; j<n; j++) {
	a[i][j] = 1.0;
	b[i][j] = 2.0;
      }
      x[i] = 1.0;
    }

    // Start time
    clock_t time0 = clock();

    // Main calculation 1: matrix-matrix product
    for (i=0; i<n; i++) {
      for (j=0; j<n; j++) {
	c[i][j] = 0.0;
	for (k=0; k<n; k++)
	  c[i][j] += a[i][k] * b[k][j];
      }
    }

    // Main calculation 2: matrix-vector product
    for (i=0; i<n; i++) {
      y[i] = 0.0;
      for (k=0; k<n; k++)
	y[i] += c[i][k] * x[k];
    }

    // Finish time
    clock_t time1 = clock();

    // Output time
    double eTime = (double)(time1-time0)/CLOCKS_PER_SEC;
    printf("%11d %15.7f\n",n, eTime);

    // Deallocation
    delete a[0]; delete b[0]; delete c[0];
    delete[] a; delete[] b; delete[] c;
    delete[] x; delete[] y;
  }
  return 0;
}

    
 
#include <stdio.h>
#include <time.h>
int main()
{
  int i, j, k;
  printf("Matrix size    Elapsed time [sec] \n");

  for (int m=1; m<=15; m++) {
    // Matrix size
    int n = m*1000;

    // Allocation
    double **a, **b, *x, *y, *z, *dum;
    dum = new double[n*n];
    a = new double*[n];
    for (i=0; i<n; i++)
      a[i] = dum + i*n;

    dum = new double[n*n];
    b = new double*[n];
    for (i=0; i<n; i++)
      b[i] = dum + i*n;






    x = new double[n];
    y = new double[n];
    z = new double[n];

    // Initialization
    for (i=0; i<n; i++) {
      for (j=0; j<n; j++) {
	a[i][j] = 1.0;
	b[i][j] = 2.0;
      }
      x[i] = 1.0;
    }

    // Start time
    clock_t time0 = clock();

    // Main calculation 1: matrix-vector product
    for (i=0; i<n; i++) {
      z[i] = 0.0;
      for (k=0; k<n; k++)
	z[i] += b[i][k] * x[k];
    }



    // Main calculation 2: matrix-vector product
    for (i=0; i<n; i++) {
      y[i] = 0.0;
      for (k=0; k<n; k++)
	y[i] += a[i][k] * z[k];
    }

    // Finish time
    clock_t time1 = clock();

    // Output time
    double eTime = (double)(time1-time0)/CLOCKS_PER_SEC;
    printf("%11d %15.7f\n",n, eTime);

    // Deallocation
    delete a[0]; delete b[0];
    delete[] a; delete[] b;
    delete[] x; delete[] y; delete[] z;
  }
  return 0;
}

    

■ 計算時間の測定結果

Code 1Code 2を実行したときの計算時間をそれぞれ青線と赤線で図1に示します。

Code 2の計算時間はCode1に比べて非常に短いので、Code 1と同じサイズNで測定すると、計算時間が測定できません。ここではCode 2のサイズNCode 1より大きい範囲にしています。

測定時間
図1 測定時間

■ 考察

図1より解かりますが、Code 1の計算時間はCode 2より非常に長いです。しかもサイズNが大きくなるにつれて、その差がどんどん開きます。ちなみにN=2000ではCode 1の計算時間は既にCode 2の5000倍程度になっています。Code 1Nが10000まで計算してもよいですが、1日ぐらいかかりそうなので、止めました。この結果より、O(N3)の恐ろしさを実感できるのではないでしょうか。



はじめに

演算数を減らす

メモリジャンプを減らす

高性能のアルゴリズム

その他



4 8 2 5 8 6