5

NEON を使用して OpenCV コードの一部を最適化しようとしています。これが私が取り組んでいる元のコードブロックです。(注: 重要な場合は、「opencvfolder/modules/video/src/lkpyramid.cpp」で完全なソースを見つけることができます。これは、オブジェクト追跡アルゴリズムの実装です。)

for( ; x < colsn; x++ )
{
    deriv_type t0 = (deriv_type)(trow0[x+cn] - trow0[x-cn]);
    deriv_type t1 = (deriv_type)((trow1[x+cn] + trow1[x-cn])*3 + trow1[x]*10);
    drow[x*2] = t0; drow[x*2+1] = t1;

}

このコードでは、deriv_type のサイズは 2 バイトです。そして、これが私が書いたNEONアセンブリです。元のコードで 10 ~ 11 fps を測定します。NEON ではさらに悪く、5 ~ 6 fps しか得られません。私は NEON についてあまり詳しくありません。おそらく、このコードには多くの間違いがあります。私はどこで間違っていますか?ありがとう

for( ; x < colsn; x+=4 )
{
    __asm__ __volatile__(
    "vld1.16 d2, [%2] \n\t" // d2 = trow0[x+cn]
    "vld1.16 d3, [%3] \n\t" // d3 = trow0[x-cn]
    "vsub.i16 d9, d2, d3 \n\t" // d9 = d2 - d3

    "vld1.16 d4, [%4] \n\t" // d4 = trow1[x+cn]
    "vld1.16 d5, [%5] \n\t" // d5 = trow1[x-cn]
    "vld1.16 d6, [%6] \n\t" // d6 = trow1[x]

    "vmov.i16 d7, #3 \n\t"  // d7 = 3
    "vmov.i16 d8, #10 \n\t" // d8 = 10


    "vadd.i16 d4, d4, d5 \n\t" // d4 = d4 + d5
    "vmul.i16 d10, d4, d7 \n\t" // d10 = d4 * d7
    "vmla.i16 d10, d6, d8 \n\t" // d10 = d10 + d6 * d8

    "vst2.16 {d9,d10}, [%0] \n\t" // drow[x*2] = d9; drow[x*2+1] = d10;
    //"vst1.16 d4, [%1] \n\t"

    :   //output
    :"r"(drow+x*2), "r"(drow+x*2+1), "r"(trow0+x+cn), "r"(trow0+x-cn), "r"(trow1+x+cn), "r"(trow1+x-cn), "r"(trow1) //input
    :"d2", "d3", "d4", "d5", "d6", "d7", "d8", "d9", "d10"  //registers


    );
}

編集

これは、組み込み関数を使用したバージョンです。以前とほぼ同じです。それはまだゆっくりと動作します。

const int16x8_t vk3 = { 3, 3, 3, 3, 3, 3, 3, 3 };
const int16x8_t vk10 = { 10, 10, 10, 10, 10, 10, 10, 10 };

for( ; x < colsn; x+=8 )
{
                int16x8x2_t loaded;
                int16x8_t t0a = vld1q_s16(&trow0[x + cn]);
                int16x8_t t0b = vld1q_s16(&trow0[x - cn]);
                loaded.val[0] = vsubq_s16(t0a, t0b); // t0 = (trow0[x + cn] - trow0[x - cn])

                loaded.val[1] = vld1q_s16(&trow1[x + cn]);
                int16x8_t t1b = vld1q_s16(&trow1[x - cn]);
                int16x8_t t1c = vld1q_s16(&trow1[x]);

                loaded.val[1] = vaddq_s16(loaded.val[1], t1b);
                loaded.val[1] = vmulq_s16(loaded.val[1], vk3);
                loaded.val[1] = vmlaq_s16(loaded.val[1], t1c, vk10);
}
4

2 に答える 2

3

データの危険性のために、多くのパイプラインストールが作成されています。たとえば、次の3つの手順:

"vadd.i16 d4, d4, d5 \n\t" // d4 = d4 + d5
"vmul.i16 d10, d4, d7 \n\t" // d10 = d4 * d7
"vmla.i16 d10, d6, d8 \n\t" // d10 = d10 + d6 * d8

それぞれ発行する命令は1つだけですが、結果の準備ができていないため、それらの間に数サイクルのストールがあります(NEON命令スケジューリング)。

ループを数回展開し、それらの命令をインターリーブしてみてください。組み込み関数を使用する場合、コンパイラがこれを行う場合があります。命令のスケジューリングなどでコンパイラを打ち負かすことは不可能ではありませんが、それは非常に難しく、多くの場合それだけの価値はありません(これは時期尚早に最適化しないことに該当する可能性があります)。

編集

あなたの本質的なコードは合理的です、私はコンパイラがあまり良い仕事をしていないのではないかと思います。生成されているアセンブリコード(objdump -d)を見てください。おそらく、パイプラインの危険性も多く発生していることがわかります。コンパイラの新しいバージョンが役立つ場合がありますが、そうでない場合は、ループを自分で変更して、結果のレイテンシを非表示にする必要があります(命令のタイミングが必要になります)。現在のコードは正しく、巧妙なコンパイラーによって最適化できるはずなので、そのままにしておいてください。

次のような結果になる可能性があります。

// do step 1 of first iteration
// ...
for (int i = 0; i < n - 1; i++) {
  // do step 1 of (i+1)th
  // do step 2 of (i)th
  // with their instructions interleaved
  // ...
}
// do step 2 of (n-1)th
// ...

ループを2つ以上のステップに分割したり、ループを数回展開したりすることもできます(たとえば、に変更、ループの本体を2倍に変更、i++後半にに変更)。この回答がお役に立てば幸いです。不明な点があればお知らせください。i+=2ii+1

于 2012-07-23T12:26:36.273 に答える
1

for ループの外に移動する必要があるいくつかのループ不変のものがあります - これは少し役立つかもしれません。

全幅 SIMD 演算の使用を検討して、ループ反復ごとに 4 ではなく 8 pint を処理できるようにすることもできます。

ただし、最も重要なのは、コンパイラがピープホールの最適化、レジスタの割り当て、命令のスケジューリング、ループの展開などを処理できるように、生の asm ではなく組み込み関数を使用する必要があることです。

例えば

// constants - init outside loop

const int16x8_t vk3 = { 3, 3, 3, 3, 3, 3, 3, 3 };
const int16x8_t vk10 = { 10, 10, 10, 10, 10, 10, 10, 10 };

for( ; x < colsn; x += 8)
{
    int16x8_t t0a = vld1q_s16(&trow0[x + cn]);
    int16x8_t t0b = vld1q_s16(&trow0[x - cn]);
    int16x8_t t0 = vsubq_s16(t0a, t0b); // t0 = (trow0[x + cn] - trow0[x - cn])

    // ...
}
于 2012-07-23T11:05:32.717 に答える