この種の質問は繰り返し発生し、スタックオーバーフローで「MATLABは高度に最適化されたライブラリを使用する」や「MATLABはMKLを使用する」よりも明確に回答する必要があります。
歴史:
行列の乗算(行列-ベクトル、ベクトル-ベクトルの乗算、および行列分解の多くと合わせて)は、線形代数における(最も重要な)問題です。エンジニアは、初期の頃からコンピュータでこれらの問題を解決してきました。
私は歴史の専門家ではありませんが、当時はどうやら、誰もが単純なループでFORTRANバージョンを書き直しました。その後、いくつかの標準化が行われ、ほとんどの線形代数問題を解決するために必要な「カーネル」(基本ルーチン)が特定されました。次に、これらの基本操作は、Basic Linear Algebra Subprograms(BLAS)と呼ばれる仕様で標準化されました。次に、エンジニアはコード内でこれらの十分にテストされたBLASルーチンを呼び出し、作業をはるかに簡単にすることができます。
BLAS:
BLASはレベル1(スカラーベクトルとベクトルベクトルの演算を定義した最初のバージョン)からレベル2(ベクトル-行列演算)からレベル3(行列-行列演算)に進化し、より多くの「カーネル」を提供して標準化を進めました。その他の基本的な線形代数演算。元のFORTRAN 77の実装は、NetlibのWebサイトで引き続き利用できます。
より良いパフォーマンスに向けて:
そのため、長年(特にBLASレベル1とレベル2のリリース間:80年代前半)、ハードウェアは変化し、ベクター演算とキャッシュ階層が登場しました。これらの進化により、BLASサブルーチンのパフォーマンスを大幅に向上させることができました。その後、さまざまなベンダーがBLASルーチンの実装を導入し、その効果はますます高まりました。
すべての歴史的な実装を知っているわけではありませんが(当時、私は生まれも子供もいませんでした)、2000年代初頭に最も注目に値する2つの実装、Intel MKLとGotoBLASが登場しました。MatlabはIntel MKLを使用します。これは非常に優れた最適化されたBLASであり、これにより、優れたパフォーマンスが説明されます。
行列乗算の技術詳細:
では、なぜMatlab(MKL)はdgemm
(倍精度の一般的な行列-行列乗算)でこれほど高速なのでしょうか。簡単に言うと、ベクトル化とデータの適切なキャッシュを使用しているためです。より複雑な言葉で:ジョナサンムーアによって提供される記事を参照してください。
基本的に、提供したC ++コードで乗算を実行する場合、キャッシュは使いやすくありません。行配列へのポインターの配列を作成したのではないかと思うので、内部ループでの「matice2」のk番目の列へのアクセスmatice2[m][k]
は非常に低速です。実際、にアクセスするときmatice2[0][k]
は、行列の配列0のk番目の要素を取得する必要があります。次に、次の反復で、matice2[1][k]
別の配列(配列1)のk番目の要素であるにアクセスする必要があります。次に、次の反復でさらに別の配列にアクセスします...行列全体matice2
が最も高いキャッシュ(8*1024*1024
バイト数が大きい)に収まらないため、プログラムはメインメモリから目的の要素をフェッチする必要があり、多くの時間。
マトリックスを転置しただけで、アクセスが連続したメモリアドレスで行われる場合、コンパイラは同時にキャッシュ内の行全体を同時にロードできるため、コードはすでにはるかに高速に実行されます。この修正バージョンを試してください:
timer.start();
float temp = 0;
//transpose matice2
for (int p = 0; p < rozmer; p++)
{
for (int q = 0; q < rozmer; q++)
{
tempmat[p][q] = matice2[q][p];
}
}
for(int j = 0; j < rozmer; j++)
{
for (int k = 0; k < rozmer; k++)
{
temp = 0;
for (int m = 0; m < rozmer; m++)
{
temp = temp + matice1[j][m] * tempmat[k][m];
}
matice3[j][k] = temp;
}
}
timer.stop();
したがって、キャッシュの局所性だけでコードのパフォーマンスが大幅に向上したことがわかります。現在、実際のdgemm
実装では、これを非常に広範囲に活用しています。TLB(Translation lookaside buffer、長いストーリー:効果的にキャッシュできるもの)のサイズによって定義された行列のブロックで乗算を実行し、プロセッサにストリーミングします。処理できるデータの量。もう1つの側面はベクトル化です。プロセッサのベクトル化された命令を使用して、最適な命令スループットを実現します。これは、クロスプラットフォームのC ++コードでは実際には実行できません。
最後に、それはStrassenのアルゴリズムまたはCoppersmith–Winogradアルゴリズムが間違っているためであると主張する人々は、上記のハードウェアの考慮事項のために、これらのアルゴリズムはどちらも実際には実装できません。