0

平均を計算して、次のコードを改善したいと思います。

void calculateMeanStDev8x8Aux(cv::Mat* patch, int sx, int sy, int& mean, float& stdev)
{

    unsigned sum=0;
    unsigned sqsum=0;
    const unsigned char* aux=patch->data + sy*patch->step + sx;
    for (int j=0; j< 8; j++) {
        const unsigned char* p = (const unsigned char*)(j*patch->step + aux ); //Apuntador al inicio de la matrix           

        for (int i=0; i<8; i++) {
            unsigned f = *p++;
            sum += f;
            sqsum += f*f;
        }           
    }       

    mean = sum >> 6;
    int r = (sum*sum) >> 6;
    stdev = sqrtf(sqsum - r);

    if (stdev < .1) {
        stdev=0;
    }
}

また、NEON 組み込み関数を使用して次のループを改善しました。

 for (int i=0; i<8; i++) {
            unsigned f = *p++;
            sum += f;
            sqsum += f*f;
        }

これは、他のループ用に改善されたコードです。

        int32x4_t vsum= { 0 };
        int32x4_t vsum2= { 0 };

        int32x4_t vsumll = { 0 };
        int32x4_t vsumlh = { 0 };
        int32x4_t vsumll2 = { 0 };
        int32x4_t vsumlh2 = { 0 };

        uint8x8_t  f= vld1_u8(p); // VLD1.8 {d0}, [r0]

        //int 16 bytes /8 elementos
        int16x8_t val =  (int16x8_t)vmovl_u8(f);

        //int 32 /4 elementos *2 
        int32x4_t vall = vmovl_s16(vget_low_s16(val));
        int32x4_t valh = vmovl_s16(vget_high_s16(val));

        // update 4 partial sum of products vectors

        vsumll2 = vmlaq_s32(vsumll2, vall, vall);
        vsumlh2 = vmlaq_s32(vsumlh2, valh, valh);

        // sum 4 partial sum of product vectors
        vsum = vaddq_s32(vall, valh);
        vsum2 = vaddq_s32(vsumll2, vsumlh2);

        // do scalar horizontal sum across final vector

        sum += vgetq_lane_s32(vsum, 0);
        sum += vgetq_lane_s32(vsum, 1);
        sum += vgetq_lane_s32(vsum, 2);
        sum += vgetq_lane_s32(vsum, 3);

        sqsum += vgetq_lane_s32(vsum2, 0);
        sqsum += vgetq_lane_s32(vsum2, 1);
        sqsum += vgetq_lane_s32(vsum2, 2);
        sqsum += vgetq_lane_s32(vsum2, 3);

しかし、それは多かれ少なかれ 30 ミリ秒遅くなります。誰かが理由を知っていますか?

すべてのコードが正しく機能しています。

4

4 に答える 4

3

ルンディンに追加。はい、レジスタベースのインデックスがあるARMのような命令セットや、即時インデックスを使用する一部のリーチは、コンパイラがインデックスを使用するように奨励するのに役立つ場合があります。また、たとえばARMはロード命令でポインタレジスタをインクリメントできますが、基本的に* p ++を1つの命令でインクリメントできます。

p[i] または p[i++] と *p または *p++ を使用すると、常にトスアップになります。一部の命令セットは、どちらのパスを使用するかがはるかに明白です。

同様にあなたのインデックス。アップの代わりにカウントダウンを使用していない場合は、ループごとに命令を節約できます。これを行う人もいます:

inc reg
cmp reg,#7
bne loop_top

ループごとに命令を保存するかもしれませんが、カウントダウンしている場合:

dec reg
bne loop_top

または私が知っている1つのプロセッサでさえ

decrement_and_jump_if_not_zero  loop_top

通常、コンパイラはこれを認識しており、推奨する必要はありません。ただし、メモリの読み取り順序が重要な p[i] 形式を使用する場合、コンパイラは読み取りの順序を任意に変更できないか、少なくとも変更すべきではありません。したがって、その場合、コードをカウントダウンする必要があります。

だから私はこれらすべてのことを試しました:

unsigned fun1 ( const unsigned char *p, unsigned *x )
{
    unsigned sum;
    unsigned sqsum;
    int i;
    unsigned f;


    sum = 0;
    sqsum = 0;
    for(i=0; i<8; i++)
    {
        f = *p++;
        sum += f;
        sqsum += f*f;
    }
    //to keep the compiler from optimizing
    //stuff out
    x[0]=sum;
    return(sqsum);
}

unsigned fun2 ( const unsigned char *p, unsigned *x  )
{
    unsigned sum;
    unsigned sqsum;
    int i;
    unsigned f;


    sum = 0;
    sqsum = 0;
    for(i=8;i--;)
    {
        f = *p++;
        sum += f;
        sqsum += f*f;
    }
    //to keep the compiler from optimizing
    //stuff out
    x[0]=sum;
    return(sqsum);
}

unsigned fun3 ( const unsigned char *p, unsigned *x )
{
    unsigned sum;
    unsigned sqsum;
    int i;

    sum = 0;
    sqsum = 0;
    for(i=0; i<8; i++)
    {
        sum += (unsigned)p[i];
        sqsum += ((unsigned)p[i])*((unsigned)p[i]);
    }
    //to keep the compiler from optimizing
    //stuff out
    x[0]=sum;
    return(sqsum);
}

unsigned fun4 ( const unsigned char *p, unsigned *x )
{
    unsigned sum;
    unsigned sqsum;
    int i;

    sum = 0;
    sqsum = 0;
    for(i=8; i;i--)
    {
        sum += (unsigned)p[i-1];
        sqsum += ((unsigned)p[i-1])*((unsigned)p[i-1]);
    }
    //to keep the compiler from optimizing
    //stuff out
    x[0]=sum;
    return(sqsum);
}

gcc と llvm (clang) の両方で。もちろん、ループは定数だったので、どちらもループを展開しました。gcc では、微妙なレジスタ ミックスの変更の場合、各実験で同じコードが生成されます。そして、少なくとも 1 つの読み取りがコードで記述された順序ではなかったので、バグであると主張します。

4つすべてのgccソリューションはこれで、一部の読み取りの並べ替えがあり、ソースコードからの読み取りが順不同であることに注意してください。これが、コードで記述された順序での読み取りに依存するハードウェア/ロジックに反する場合、大きな問題が発生します。

00000000 <fun1>:
   0:   e92d05f0    push    {r4, r5, r6, r7, r8, sl}
   4:   e5d06001    ldrb    r6, [r0, #1]
   8:   e00a0696    mul sl, r6, r6
   c:   e4d07001    ldrb    r7, [r0], #1
  10:   e02aa797    mla sl, r7, r7, sl
  14:   e5d05001    ldrb    r5, [r0, #1]
  18:   e02aa595    mla sl, r5, r5, sl
  1c:   e5d04002    ldrb    r4, [r0, #2]
  20:   e02aa494    mla sl, r4, r4, sl
  24:   e5d0c003    ldrb    ip, [r0, #3]
  28:   e02aac9c    mla sl, ip, ip, sl
  2c:   e5d02004    ldrb    r2, [r0, #4]
  30:   e02aa292    mla sl, r2, r2, sl
  34:   e5d03005    ldrb    r3, [r0, #5]
  38:   e02aa393    mla sl, r3, r3, sl
  3c:   e0876006    add r6, r7, r6
  40:   e0865005    add r5, r6, r5
  44:   e0854004    add r4, r5, r4
  48:   e5d00006    ldrb    r0, [r0, #6]
  4c:   e084c00c    add ip, r4, ip
  50:   e08c2002    add r2, ip, r2
  54:   e082c003    add ip, r2, r3
  58:   e023a090    mla r3, r0, r0, sl
  5c:   e080200c    add r2, r0, ip
  60:   e5812000    str r2, [r1]
  64:   e1a00003    mov r0, r3
  68:   e8bd05f0    pop {r4, r5, r6, r7, r8, sl}
  6c:   e12fff1e    bx  lr

ロードのインデックスと微妙なレジスタの混合は、gcc との関数間の唯一の違いであり、すべての操作は同じ順序で同じでした。

llvm/clang:

00000000 <fun1>:
   0:   e92d41f0    push    {r4, r5, r6, r7, r8, lr}
   4:   e5d0e000    ldrb    lr, [r0]
   8:   e5d0c001    ldrb    ip, [r0, #1]
   c:   e5d03002    ldrb    r3, [r0, #2]
  10:   e5d08003    ldrb    r8, [r0, #3]
  14:   e5d04004    ldrb    r4, [r0, #4]
  18:   e5d05005    ldrb    r5, [r0, #5]
  1c:   e5d06006    ldrb    r6, [r0, #6]
  20:   e5d07007    ldrb    r7, [r0, #7]
  24:   e08c200e    add r2, ip, lr
  28:   e0832002    add r2, r3, r2
  2c:   e0882002    add r2, r8, r2
  30:   e0842002    add r2, r4, r2
  34:   e0852002    add r2, r5, r2
  38:   e0862002    add r2, r6, r2
  3c:   e0870002    add r0, r7, r2
  40:   e5810000    str r0, [r1]
  44:   e0010e9e    mul r1, lr, lr
  48:   e0201c9c    mla r0, ip, ip, r1
  4c:   e0210393    mla r1, r3, r3, r0
  50:   e0201898    mla r0, r8, r8, r1
  54:   e0210494    mla r1, r4, r4, r0
  58:   e0201595    mla r0, r5, r5, r1
  5c:   e0210696    mla r1, r6, r6, r0
  60:   e0201797    mla r0, r7, r7, r1
  64:   e8bd41f0    pop {r4, r5, r6, r7, r8, lr}
  68:   e1a0f00e    mov pc, lr

おそらく、キャッシュについて考えて、読み取りをすべて一度に行うことができます。llvm は、少なくとも 1 つのケースで読み取りの順序が狂っていました。

00000144 <fun4>:
 144:   e92d40f0    push    {r4, r5, r6, r7, lr}
 148:   e5d0c007    ldrb    ip, [r0, #7]
 14c:   e5d03006    ldrb    r3, [r0, #6]
 150:   e5d02005    ldrb    r2, [r0, #5]
 154:   e5d05004    ldrb    r5, [r0, #4]
 158:   e5d0e000    ldrb    lr, [r0]
 15c:   e5d04001    ldrb    r4, [r0, #1]
 160:   e5d06002    ldrb    r6, [r0, #2]
 164:   e5d00003    ldrb    r0, [r0, #3]

はい、ラムからいくつかの値を平均化するために、順序は問題ではありません。

そのため、コンパイラは展開されたパスを選択し、マイクロ最適化を気にしませんでした。ループのサイズのため、両方ともループごとにロードされた値の 1 つを保持する一連のレジスタを焼き付けてから、それらの一時的な読み取りからの加算または乗算を実行することを選択します。ループのサイズを少し大きくすると、展開されたループがレジスターを使い果たしたときに sum と sqsum の累積が見られると予想されます。または、ループを展開しないことを選択した場所でしきい値に達します。

長さを渡し、上記のコードの 8 を渡された長さに置き換えると、コンパイラはこれからループを作成するように強制されます。最適化が表示されます。次のような命令が使用されます。

  a4:   e4d35001    ldrb    r5, [r3], #1

そして、アームであるため、ループレジスタを1か所で変更し、後で命令数と等しくない場合は分岐します...可能なためです。

確かにこれは数学関数ですが、float を使用するのは面倒です。そして、乗算を使用するのは苦痛であり、除算はさらに悪化します。幸いなことに、シフトが使用されました。幸いなことに、これはシフトを使用できるように符号なしでした(符号付きの数値に対して除算を使用した場合、利用可能な場合、コンパイラは算術シフトを使用することを知っていたはずです)。

したがって、内部ループは複数回実行されるため、基本的には内部ループのマイクロ最適化に焦点を当てます。これを変更できる場合は、可能であればシフトして追加するか、データを配置してループから取り出すことができます (可能であれば)。 、これを行うために他のコピーループを他の場所で無駄にしないでください)

const unsigned char* p = (const unsigned char*)(j*patch->step + aux );

速度を上げることができます。私はそれを試しませんでしたが、ループ内のループであるため、コンパイラはおそらくそのループを展開しません...

簡単に言えば、命令セットによっては、愚かなコンパイラに対していくらかの利益が得られる可能性がありますが、このコードはそれほど悪くないため、コンパイラは可能な限り最適化できます。

于 2011-12-15T16:08:00.223 に答える
1

まず第一に、代わりにコードレビューに投稿すると、このようなものについて非常に優れた詳細な回答が得られるでしょう。

効率と疑わしい変数タイプに関するコメント:

unsigned f = *p++;p配列のインデックスを使用してアクセスし、p[i]を使用してデータにアクセスする方がよいでしょう。これは、コンパイラ、キャッシュメモリの最適化などに大きく依存します(一部のARMの第一人者は、この件に関して私よりも優れたアドバイスを提供できます)。

ところで、intへのconstchar全体は非常に疑わしいように見えます。これらの文字は8ビットの符号なし整数と見なされると思いますか?標準Cuint8_tは、これに適したタイプである可能性が高く、char回避したいさまざまな未定義の署名の問題があります。

また、なぜとのワイルドミキシングをunsignedしているのintですか?あなたは暗黙の整数バランシングのバグを求めています。

stdev < .1。ちょっとしたことです。これをに変更する.1fか、.1はdoubleリテラルであるため、floatをdoubleに暗黙的に昇格させます。

于 2011-12-15T12:46:42.240 に答える
1

データは 8 バイトのグループで読み取られるため、ハードウェア バスと配列自体の配置に応じて、単一の long long 読み取りを介して内側のループを読み取ることで、おそらくいくらかの利益を得ることができます。数値を個別の値に変換するか、ARM 組み込み関数を使用して、add8 命令を使用してインライン asm と並行して加算を行うか (1 つのレジスタで一度に 4 つの数値を加算する)、少しシフトして add16 を使用して値をオーバーフローさせます。 16ビット相当のスペースに。デュアル符号付き乗算および累算命令もあり、最初の累算ループは ARM を介してほぼ完全にサポートされます。また、入ってくるデータが 16 ビット値になるように処理できれば、これも高速化できます。

NEON が遅い理由については、ベクトルを設定する際のオーバーヘッドと、より大きな型でプッシュしている追加のデータが、このような小さなデータ セットで得られるパフォーマンスを低下させていると思います。元のコードは、最初から非常に ARM に適しています。つまり、セットアップのオーバーヘッドがおそらくあなたを殺しています。疑わしい場合は、アセンブリの出力を確認してください。それは、何が本当に起こっているかを教えてくれます。おそらく、組み込み関数を使用しようとすると、コンパイラがデータをあちこちにプッシュしたりポップしたりしています。この種の動作を見たのは初めてではありません。

于 2011-12-15T17:26:45.967 に答える
0

Lundin、dwelch、Michelに感謝します。私は次の改善を行いました、そしてそれは私のコードにとって最良のようです。キャッシュにアクセスするのは1回だけなので、キャッシュアクセスを改善するためにサイクル数を減らしようとしています。

int step=patch->step;
 for (int j=0; j< 8; j++) {
        p = (uint8_t*)(j*step + aux ); /

        i=8;
        do {                
            f=p[i];
            sum += f;
            sqsum += f*f;

        } while(--i);

}
于 2011-12-16T11:52:08.903 に答える