これは問題のプログラムからの抜粋です。行列img[][]
はサイズSIZE×SIZEを持ち、次のように初期化されます。
img[j][i] = 2 * j + i
次に、行列res[][]
を作成します。ここにある各フィールドは、img行列内でその周囲の9つのフィールドの平均になるようにします。簡単にするために、境界は0のままにしておきます。
for(i=1;i<SIZE-1;i++)
for(j=1;j<SIZE-1;j++) {
res[j][i]=0;
for(k=-1;k<2;k++)
for(l=-1;l<2;l++)
res[j][i] += img[j+l][i+k];
res[j][i] /= 9;
}
これでプログラムは終わりです。完全を期すために、これが前に来るものです。後にコードはありません。ご覧のとおり、初期化だけです。
#define SIZE 8192
float img[SIZE][SIZE]; // input image
float res[SIZE][SIZE]; //result of mean filter
int i,j,k,l;
for(i=0;i<SIZE;i++)
for(j=0;j<SIZE;j++)
img[j][i] = (2*j+i)%8196;
基本的に、このプログラムはSIZEが2048の倍数の場合は遅くなります。実行時間
SIZE = 8191: 3.44 secs
SIZE = 8192: 7.20 secs
SIZE = 8193: 3.18 secs
コンパイラはGCCです。私が知っていることから、これはメモリ管理のせいですが、私はその問題についてあまり知りません。そのため、ここで質問します。
またこれを修正する方法はいいでしょうが、誰かがこれらの実行時間を説明できれば私はもう十分満足しているでしょう。
私はすでにmalloc/freeを知っていますが、問題は使用されるメモリの量ではなく、単に実行時間なので、それがどのように役立つかわかりません。
違いは、次の関連質問と同じスーパーアライメントの問題が原因です。
しかしそれは、コードにもう1つ問題があるからです。
元のループから始めます。
for(i=1;i<SIZE-1;i++)
for(j=1;j<SIZE-1;j++) {
res[j][i]=0;
for(k=-1;k<2;k++)
for(l=-1;l<2;l++)
res[j][i] += img[j+l][i+k];
res[j][i] /= 9;
}
まず、2つの内側のループが簡単であることに注目してください。以下のように展開することができます。
for(i=1;i<SIZE-1;i++) {
for(j=1;j<SIZE-1;j++) {
res[j][i]=0;
res[j][i] += img[j-1][i-1];
res[j][i] += img[j ][i-1];
res[j][i] += img[j+1][i-1];
res[j][i] += img[j-1][i ];
res[j][i] += img[j ][i ];
res[j][i] += img[j+1][i ];
res[j][i] += img[j-1][i+1];
res[j][i] += img[j ][i+1];
res[j][i] += img[j+1][i+1];
res[j][i] /= 9;
}
}
それで、私たちが興味を持っている2つの外側ループが残ります。
この質問でも問題が同じであることがわかります。 2D配列を反復処理するときにループの順序がパフォーマンスに影響するのはなぜですか?
行列を行ごとではなく列ごとに繰り返しています。
この問題を解決するには、2つのループを交換する必要があります。
for(j=1;j<SIZE-1;j++) {
for(i=1;i<SIZE-1;i++) {
res[j][i]=0;
res[j][i] += img[j-1][i-1];
res[j][i] += img[j ][i-1];
res[j][i] += img[j+1][i-1];
res[j][i] += img[j-1][i ];
res[j][i] += img[j ][i ];
res[j][i] += img[j+1][i ];
res[j][i] += img[j-1][i+1];
res[j][i] += img[j ][i+1];
res[j][i] += img[j+1][i+1];
res[j][i] /= 9;
}
}
これにより、すべての非順次アクセスが完全に排除されるため、2のべき乗でランダムなスローダウンが発生することはなくなります。
Core i7 920 @ 3.5 GHz
元のコード
8191: 1.499 seconds
8192: 2.122 seconds
8193: 1.582 seconds
交換された外部ループ:
8191: 0.376 seconds
8192: 0.357 seconds
8193: 0.351 seconds
次のテストは、デフォルトのQt Creatorインストールで使用されているので、Visual C++コンパイラを使用して行われました(最適化フラグはありません)。 GCCを使うとき、Mysticalのバージョンと私の「最適化された」コードの間に大きな違いはありません。したがって、結論としては、コンパイラーの最適化は人間よりもマイクロ最適化のほうがうまくいくということです(ついに私は)。私は答えの残りを参考のために残します。
このように画像を処理するのは効率的ではありません。一次元配列を使用することをお勧めします。すべてのピクセルを処理することは1つのループで行われます。ポイントへのランダムアクセスは、次のようにして行うことができます。
pointer + (x + y*width)*(sizeOfOnePixel)
この特定のケースでは、3つのピクセルグループの合計を水平に計算してキャッシュする方が良いでしょう。なぜなら、それらはそれぞれ3回使用されるからです。
いくつかテストをしましたが、共有する価値があると思います。各結果は平均5回のテストです。
User1615209によるオリジナルコード:
8193: 4392 ms
8192: 9570 ms
Mysticalのバージョン:
8193: 2393 ms
8192: 2190 ms
1次元配列を使用した2回のパス。最初は水平方向の合計、2回目は垂直方向の合計と平均です。 3つのポインタを使用して2パスアドレッシングを行い、増分を次のように増やすだけです。
imgPointer1 = &avg1[0][0];
imgPointer2 = &avg1[0][SIZE];
imgPointer3 = &avg1[0][SIZE+SIZE];
for(i=SIZE;i<totalSize-SIZE;i++){
resPointer[i]=(*(imgPointer1++)+*(imgPointer2++)+*(imgPointer3++))/9;
}
8193: 938 ms
8192: 974 ms
2次元1次元配列を使用して、次のようにアドレス指定します。
for(i=SIZE;i<totalSize-SIZE;i++){
resPointer[i]=(hsumPointer[i-SIZE]+hsumPointer[i]+hsumPointer[i+SIZE])/9;
}
8193: 932 ms
8192: 925 ms
ワンパスキャッシュの水平方向の合計は1行先になるため、キャッシュに残ります。
// Horizontal sums for the first two lines
for(i=1;i<SIZE*2;i++){
hsumPointer[i]=imgPointer[i-1]+imgPointer[i]+imgPointer[i+1];
}
// Rest of the computation
for(;i<totalSize;i++){
// Compute horizontal sum for next line
hsumPointer[i]=imgPointer[i-1]+imgPointer[i]+imgPointer[i+1];
// Final result
resPointer[i-SIZE]=(hsumPointer[i-SIZE-SIZE]+hsumPointer[i-SIZE]+hsumPointer[i])/9;
}
8193: 599 ms
8192: 652 ms
結論:
私はそれがはるかに良いことが可能であると確信しています。
NOTEMysticalの優れた答えで説明されているキャッシュの問題ではなく、一般的なパフォーマンスの問題をターゲットとしてこの答えを書いたことに注意してください。最初は疑似コードでした。私はコメントでテストをするように頼まれました...これはテストで完全にリファクタリングされたバージョンです。