したがって、行列を転置するための明らかな方法は次を使用することです:
for( int i = 0; i < n; i++ )
for( int j = 0; j < n; j++ )
destination[j+i*n] = source[i+j*n];
しかし、ローカリティとキャッシュブロッキングを利用するものが必要です。私はそれを調べていたので、これを行うコードは見つかりませんでしたが、元のコードを非常に簡単に修正する必要があると言われています。何か案は?
編集:2000x2000のマトリックスがあり、2つのfor
ループを使用してコードを変更し、基本的にマトリックスを個別に転置するブロック、たとえば2x2ブロックまたは40x40ブロックに分割し、どのブロックサイズが最も効率的ですか。
Edit2:マトリックスは、列のメジャー順に格納されます。つまり、マトリックスの場合
a1 a2
a3 a4
a1 a3 a2 a4
として保存されます。
おそらく4つのループが必要になります。2つはブロックを反復処理し、もう2つは単一のブロックの転置コピーを実行します。簡単にするために、マトリックスのサイズを分割するブロックサイズを想定します。これは次のように思えますが、封筒の裏にいくつかの絵を描きたいと思います。
for (int i = 0; i < n; i += blocksize) {
for (int j = 0; j < n; j += blocksize) {
// transpose the block beginning at [i,j]
for (int k = i; k < i + blocksize; ++k) {
for (int l = j; l < j + blocksize; ++l) {
dst[k + l*n] = src[l + k*n];
}
}
}
}
さらに重要な洞察は、実際にはこのためのキャッシュ忘却アルゴリズムがあることです( http://en.wikipedia.org/wiki/Cache-oblivious_algorithm を参照してください。この問題を例として使用しています)。 「キャッシュ忘却型」の非公式の定義では、良好な/最適なキャッシュパフォーマンスを実現するために、パラメーター(この場合はブロックサイズ)を微調整する必要はありません。この場合の解決策は、行列を再帰的に半分に分割して転置し、半分を宛先の正しい位置に転置することです。
キャッシュサイズが実際に何であれ、この再帰はそれを利用します。あなたの戦略と比較して、余分な管理オーバーヘッドが少しあると思います。パフォーマンス実験を使用して、実際にはキャッシュ内で実際に開始される再帰のポイントに直接ジャンプし、それ以上先に進むことはありません。一方、パフォーマンスの実験では、顧客のマシンではなく、マシンで機能する答えが得られる場合があります。
昨日もまったく同じ問題がありました。私はこの解決策になりました:
void transpose(double *dst, const double *src, size_t n, size_t p) noexcept {
THROWS();
size_t block = 32;
for (size_t i = 0; i < n; i += block) {
for(size_t j = 0; j < p; ++j) {
for(size_t b = 0; b < block && i + b < n; ++b) {
dst[j*n + i + b] = src[(i + b)*p + j];
}
}
}
}
これは私のマシンで明らかな解決策よりも4倍高速です。
このソリューションは、ブロックサイズの倍数ではない次元を持つ長方形の行列を処理します。
dstとsrcが同じ正方行列の場合、代わりにインプレース関数を実際に使用する必要があります。
void transpose(double*m,size_t n)noexcept{
size_t block=0,size=8;
for(block=0;block+size-1<n;block+=size){
for(size_t i=block;i<block+size;++i){
for(size_t j=i+1;j<block+size;++j){
std::swap(m[i*n+j],m[j*n+i]);}}
for(size_t i=block+size;i<n;++i){
for(size_t j=block;j<block+size;++j){
std::swap(m[i*n+j],m[j*n+i]);}}}
for(size_t i=block;i<n;++i){
for(size_t j=i+1;j<n;++j){
std::swap(m[i*n+j],m[j*n+i]);}}}
C++ 11を使用しましたが、これは他の言語に簡単に翻訳できます。
メモリ内の行列を転置する代わりに、転置操作を、マトリックスで実行する次の操作に折り畳みませんか?
Steve Jessopは、キャッシュ忘却型行列転置アルゴリズムに言及しました。記録のために、キャッシュ忘却型行列転置の可能な実装を共有したいと思います。
public class Matrix {
protected double data[];
protected int rows, columns;
public Matrix(int rows, int columns) {
this.rows = rows;
this.columns = columns;
this.data = new double[rows * columns];
}
public Matrix transpose() {
Matrix C = new Matrix(columns, rows);
cachetranspose(0, rows, 0, columns, C);
return C;
}
public void cachetranspose(int rb, int re, int cb, int ce, Matrix T) {
int r = re - rb, c = ce - cb;
if (r <= 16 && c <= 16) {
for (int i = rb; i < re; i++) {
for (int j = cb; j < ce; j++) {
T.data[j * rows + i] = data[i * columns + j];
}
}
} else if (r >= c) {
cachetranspose(rb, rb + (r / 2), cb, ce, T);
cachetranspose(rb + (r / 2), re, cb, ce, T);
} else {
cachetranspose(rb, re, cb, cb + (c / 2), T);
cachetranspose(rb, re, cb + (c / 2), ce, T);
}
}
}
キャッシュ忘却型アルゴリズムの詳細については、 こちら をご覧ください。
行列の乗算 が思い浮かびますが、各要素が読み取られるため、キャッシュの問題はより顕著になります[〜#〜] n [〜#〜]回。
行列の転置では、単一の線形パスで読み取りを行いますが、それを最適化する方法はありません。ただし、複数の行を同時に処理して、複数の列を書き込み、キャッシュライン全体を埋めることができます。必要なループは3つだけです。
または、逆にそれを行い、線形に書き込みながら列を読み取ります。
大きなマトリックス、場合によっては大きなスパースマトリックスでは、それをより小さなキャッシュフレンドリーチャンク(Say、4x4サブマトリックス)に分解することをお勧めします。最適化されたコードパスの作成に役立つサブマトリックスにIDとしてフラグを付けることもできます。