1

複数のフル解像度画像のガウス ピラミッドとラプラシアン ピラミッドを計算する必要がある Android アプリを作成しようとしています。これを NDK を使用して C++ で作成しました。コードの最も重要な部分は、画像にガウス フィルターを適用することです。このフィルターを適用しています。水平方向と垂直方向。

フィルターは (0.0625, 0.25, 0.375, 0.25, 0.0625) です。整数を扱っているので、(1, 4, 6, 4, 1)/16 を計算しています。

dst[index] = ( src[index-2] + src[index-1]*4 + src[index]*6+src[index+1]*4+src[index+2])/16;

私はいくつかの簡単な最適化を行いましたが、まだ予想よりも遅く動作しており、他に不足している最適化オプションがあるかどうか疑問に思っていました.

PS: インライン アーム アセンブリを使用してこのフィルター パーツを作成しようとしましたが、結果が 2 倍遅くなりました。

//horizontal  filter
for(unsigned y = 0; y < height;  y++) {
    for(unsigned x = 2; x < width-2;  x++) {
        int index = y*width+x;
            dst[index].r = (src[index-2].r+ src[index+2].r + (src[index-1].r + src[index+1].r)*4 + src[index].r*6)>>4;
            dst[index].g = (src[index-2].g+ src[index+2].g + (src[index-1].g + src[index+1].g)*4 + src[index].g*6)>>4;
            dst[index].b = (src[index-2].b+ src[index+2].b + (src[index-1].b + src[index+1].b)*4 + src[index].b*6)>>4;                
     }
}
//vertical filter
for(unsigned y = 2;  y < height-2;  y++) {
    for(unsigned x = 0;  x < width;  x++) {
        int index = y*width+x;
            dst[index].r = (src[index-2*width].r + src[index+2*width].r  + (src[index-width].r + src[index+width].r)*4 + src[index].r*6)>>4;
            dst[index].g = (src[index-2*width].g + src[index+2*width].g  + (src[index-width].g + src[index+width].g)*4 + src[index].g*6)>>4;
            dst[index].b = (src[index-2*width].b + src[index+2*width].b  + (src[index-width].b + src[index+width].b)*4 + src[index].b*6)>>4;
     }
}
4

3 に答える 3

4

乗算はが変更され indexた場合にのみ発生するため、内側のループから乗算を除外できます。y

for (unsigned y ...
{
    int index = y * width;
    for (unsigned int x...  

変数を使用する前にロードすることで、速度が向上する場合があります。これにより、プロセッサはそれらをキャッシュにロードします。

for (unsigned x = ...  
{  
    register YOUR_DATA_TYPE a, b, c, d, e;
    a = src[index - 2].r;
    b = src[index - 1].r;
    c = src[index + 0].r; // The " + 0" is to show a pattern.
    d = src[index + 1].r;
    e = src[index + 2].r;
    dest[index].r = (a + e + (b + d) * 4 + c * 6) >> 4;
    // ...  

src[index+2]もう 1 つのトリックは、src の値を「キャッシュ」して、値が 5 回まで使用される可能性がある ため、毎回新しい値のみが追加されるようにすることです。

したがって、ここに概念の例を示します。

//horizontal  filter
for(unsigned y = 0; y < height;  y++)
{
    int index = y*width + 2;
    register YOUR_DATA_TYPE a, b, c, d, e;
    a = src[index - 2].r;
    b = src[index - 1].r;
    c = src[index + 0].r; // The " + 0" is to show a pattern.
    d = src[index + 1].r;
    e = src[index + 2].r;
    for(unsigned x = 2; x < width-2;  x++)
    {
        dest[index - 2 + x].r = (a + e + (b + d) * 4 + c * 6) >> 4;
        a = b;
        b = c;
        c = d;
        d = e;
        e = src[index + x].r;
于 2012-10-24T22:38:22.770 に答える
2

あなたのコンパイラがこれらすべてをどのように最適化するかはわかりませんが、私はポインターで作業する傾向があります。構造体が 3 バイトであると仮定すると、適切な場所 (ソースのフィルターの端、ターゲットの宛先) のポインターから開始し、定数配列オフセットを使用してそれらを移動するだけです。また、外側のループにオプションの OpenMP ディレクティブを追加しました。これによっても状況が改善される可能性があります。

#pragma omp parallel for
for(unsigned y = 0; y < height;  y++) {
    const int rowindex = y * width;
    char * dpos = (char*)&dest[rowindex+2];
    char * spos = (char*)&src[rowindex];
    const char *end = (char*)&src[rowindex+width-2];

    for( ; spos != end;  spos++, dpos++) {
        *dpos = (spos[0] + spos[4] + ((spos[1] + src[3])<<2) + spos[2]*6) >> 4;
    }
}

垂直ループについても同様です。

const int scanwidth = width * 3;
const int row1 = scanwidth;
const int row2 = row1+scanwidth;
const int row3 = row2+scanwidth;
const int row4 = row3+scanwidth;

#pragma omp parallel for
for(unsigned y = 2;  y < height-2;  y++) {
    const int rowindex = y * width;
    char * dpos = (char*)&dest[rowindex];
    char * spos = (char*)&src[rowindex-row2];
    const char *end = spos + scanwidth;

    for( ; spos != end;  spos++, dpos++) {
        *dpos = (spos[0] + spos[row4] + ((spos[row1] + src[row3])<<2) + spos[row2]*6) >> 4;
    }
}

とにかく、これは私が畳み込みを行う方法です。読みやすさが少し犠牲になりますが、違いを測定しようとしたことはありません。私は最初からそのように書く傾向があります。それがあなたにスピードアップを与えるかどうか見てください。マルチコア マシンを使用している場合、OpenMP は確実に機能し、ポインター.

これらの操作に SSE を使用することについてのコメントが気に入っています。

于 2012-10-24T22:31:22.297 に答える
1

より明白な最適化のいくつかは、カーネルの対称性を利用しています。

a=*src++;    b=*src++;    c=*src++;    d=*src++;    e=*src++; // init

LOOP (n/5) times:
z=(a+e)+(b+d)<<2+c*6;  *dst++=z>>4;  // then reuse the local variables
a=*src++;
z=(b+a)+(c+e)<<2+d*6;  *dst++=z>>4;  // registers have been read only once...
b=*src++;
z=(c+b)+(d+a)<<2+e*6;  *dst++=z>>4;
e=*src++;

2 つ目は、1 つの整数を使用して複数の加算を実行できることです。フィルタリングする値が符号なしの場合、1 つの 32 ビット整数に 2 つのチャネル (または 64 ビット整数に 4 つのチャネル) を収めることができます。貧乏人のSIMDです。

a=  0x[0011][0034]  <-- split to two 
b=  0x[0031][008a]
----------------------
sum    0042  00b0
>>4    0004  200b0  <-- mask off
mask   00ff  00ff   
-------------------
       0004  000b   <-- result 

(シミュレートされた SIMD は、1 つの加算とそれに続く 4 のシフトを示しています)

これは、3 つの rgb 操作を並行して計算するカーネルです (64 ビット アーキテクチャで 6 つの rgb 操作に変更するのは簡単です...)。

#define MASK (255+(255<<10)+(255<<20))
#define KERNEL(a,b,c,d,e) { \
 a=((a+e+(c<<1))>>2) & MASK; a=(a+b+c+d)>>2 & MASK; *DATA++ = a; a=DATA[4]; }

void calc_5_rgbs(unsigned int *DATA)
{
   register unsigned int a = DATA[0], b=DATA[1], c=DATA[2], d=DATA[3], e=DATA[4];
   KERNEL(a,b,c,d,e);
   KERNEL(b,c,d,e,a);
   KERNEL(c,d,e,a,b);
   KERNEL(d,e,a,b,c);
   KERNEL(e,a,b,c,d);
}

ARM および 16 レジスタを備えた 64 ビット IA で最適に動作します... 32 ビット IA でのレジスタ不足を克服するために、アセンブラの最適化が必要です (たとえば、ebp を GPR として使用します)。そして、それがインプレースアルゴリズムであるという理由だけで...

データの 8 ビットごとに 2 つのガーディアン ビットがあるだけで、整数計算とまったく同じ結果が得られます。

そしてところで:r、g、b要素よりもバイトごとに配列バイトを実行する方が高速です

 unsigned char *s=(unsigned char *) source_array; 
 unsigned char *d=(unsigned char *) dest_array; 
 for (j=0;j<3*N;j++) d[j]=(s[j]+s[j+16]+s[j+8]*6+s[j+4]*4+s[j+12]*4)>>4;
于 2012-10-25T11:17:25.117 に答える