3

NEON が C のように遅いのはなぜでしょうか?

私は、入力値に値を割り当てることにより、入力値を範囲にバケット化する高速ヒストグラム関数を構築しようとしています。これは、最も近い範囲のしきい値です。これは画像に適用されるものなので、高速である必要があります (画像配列が 640x480 で 300,000 要素であると仮定します)。ヒストグラム範囲の数値は (0,25,50,75,100) の倍数です。入力は浮動小数点数で、最終出力は明らかに整数になります

新しい空のプロジェクト (アプリ デリゲートなし) を開き、main.m ファイルのみを使用して、xCode で次のバージョンをテストしました。Accelerate を除いて、リンクされているすべてのライブラリを削除しました。

これが C の実装です。古いバージョンでは if then がたくさんありましたが、最終的に最適化されたロジックは次のとおりです。11秒と300ミリ秒かかりました。

int main(int argc, char *argv[])
{
  NSLog(@"starting");

  int sizeOfArray=300000;

  float* inputArray=(float*) malloc(sizeof(float)*sizeOfArray);
  int* outputArray=(int*) malloc(sizeof(int)*sizeOfArray);

  for (int i=0; i<sizeOfArray; ++i)
  {
    inputArray[i]=88.5;
  }

  //Assume range is [0,25,50,75,100]
  int lcd=25;

  for (int j=0; j<1000; ++j)// just to get some good time interval
  {
    for (int i=0; i<sizeOfArray; ++i)
    {
        //a 60.5 would give a 50. An 88.5 would give 100
        outputArray[i]=roundf(inputArray[i]/lcd)*lcd;
    }
  }
NSLog(@"done");
}

これが vDSP の実装です。面倒な整数への浮動小数点演算を行ったり来たりしても、わずか 6 秒しかかかりませんでした。ほぼ 50% の改善!

//vDSP implementation
 int main(int argc, char *argv[])
 {
   NSLog(@"starting");

   int sizeOfArray=300000;

   float* inputArray=(float*) malloc(sizeof(float)*sizeOfArray);
   float* outputArrayF=(float*) malloc(sizeof(float)*sizeOfArray);//vDSP requires matching of input output
   int* outputArray=(int*) malloc(sizeof(int)*sizeOfArray); //rounded value to the nearest integere
   float* finalOutputArrayF=(float*) malloc(sizeof(float)*sizeOfArray);
   int* finalOutputArray=(int*) malloc(sizeof(int)*sizeOfArray); //to compare apples to apples scenarios output


   for (int i=0; i<sizeOfArray; ++i)
   {
     inputArray[i]=37.0; //this will produce an final number of 25. On the other hand 37.5 would produce 50.
   }


   for (int j=0; j<1000; ++j)// just to get some good time interval
   {
     //Assume range is [0,25,50,75,100]
     float lcd=25.0f;

     //divide by lcd
     vDSP_vsdiv(inputArray, 1, &lcd, outputArrayF, 1,sizeOfArray);

     //Round to nearest integer
     vDSP_vfixr32(outputArrayF, 1,outputArray, 1, sizeOfArray);

     // MUST convert int to float (cannot just cast) then multiply by scalar - This step has the effect of rounding the number to the nearest lcd.
    vDSP_vflt32(outputArray, 1, outputArrayF, 1, sizeOfArray);
    vDSP_vsmul(outputArrayF, 1, &lcd, finalOutputArrayF, 1, sizeOfArray);
    vDSP_vfix32(finalOutputArrayF, 1, finalOutputArray, 1, sizeOfArray);
   }
  NSLog(@"done");
}

これが Neon の実装です。初めてなので仲良く遊んでね!vDSP より遅く、9 秒と 300 ミリ秒かかりましたが、私には意味がありませんでした。vDSP が NEON よりも最適化されているか、何か間違ったことをしています。

//NEON implementation
int main(int argc, char *argv[])
{
NSLog(@"starting");

int sizeOfArray=300000;

float* inputArray=(float*) malloc(sizeof(float)*sizeOfArray);
float* finalOutputArrayF=(float*) malloc(sizeof(float)*sizeOfArray);

for (int i=0; i<sizeOfArray; ++i)
{
    inputArray[i]=37.0; //this will produce an final number of 25. On the other hand 37.5 would produce 50.
}



for (int j=0; j<1000; ++j)// just to get some good time interval
{
    float32x4_t c0,c1,c2,c3;
    float32x4_t e0,e1,e2,e3;
    float32x4_t f0,f1,f2,f3;

    //ranges of histogram buckets
    float32x4_t buckets0=vdupq_n_f32(0);
    float32x4_t buckets1=vdupq_n_f32(25);
    float32x4_t buckets2=vdupq_n_f32(50);
    float32x4_t buckets3=vdupq_n_f32(75);
    float32x4_t buckets4=vdupq_n_f32(100);

    //midpoints of ranges
    float32x4_t thresholds1=vdupq_n_f32(12.5);
    float32x4_t thresholds2=vdupq_n_f32(37.5);
    float32x4_t thresholds3=vdupq_n_f32(62.5);
    float32x4_t thresholds4=vdupq_n_f32(87.5);


    for (int i=0; i<sizeOfArray;i+=16)
    {
        c0= vld1q_f32(&inputArray[i]);//load
        c1= vld1q_f32(&inputArray[i+4]);//load
        c2= vld1q_f32(&inputArray[i+8]);//load
        c3= vld1q_f32(&inputArray[i+12]);//load


        f0=buckets0;
        f1=buckets0;
        f2=buckets0;
        f3=buckets0;

        //register0
        e0=vcgtq_f32(c0,thresholds1);
        f0=vbslq_f32(e0, buckets1, f0);

        e0=vcgtq_f32(c0,thresholds2);
        f0=vbslq_f32(e0, buckets2, f0);

        e0=vcgtq_f32(c0,thresholds3);
        f0=vbslq_f32(e0, buckets3, f0);

        e0=vcgtq_f32(c0,thresholds4);
        f0=vbslq_f32(e0, buckets4, f0);



        //register1
        e1=vcgtq_f32(c1,thresholds1);
        f1=vbslq_f32(e1, buckets1, f1);

        e1=vcgtq_f32(c1,thresholds2);
        f1=vbslq_f32(e1, buckets2, f1);

        e1=vcgtq_f32(c1,thresholds3);
        f1=vbslq_f32(e1, buckets3, f1);

        e1=vcgtq_f32(c1,thresholds4);
        f1=vbslq_f32(e1, buckets4, f1);


        //register2
        e2=vcgtq_f32(c2,thresholds1);
        f2=vbslq_f32(e2, buckets1, f2);

        e2=vcgtq_f32(c2,thresholds2);
        f2=vbslq_f32(e2, buckets2, f2);

        e2=vcgtq_f32(c2,thresholds3);
        f2=vbslq_f32(e2, buckets3, f2);

        e2=vcgtq_f32(c2,thresholds4);
        f2=vbslq_f32(e2, buckets4, f2);


        //register3
        e3=vcgtq_f32(c3,thresholds1);
        f3=vbslq_f32(e3, buckets1, f3);

        e3=vcgtq_f32(c3,thresholds2);
        f3=vbslq_f32(e3, buckets2, f3);

        e3=vcgtq_f32(c3,thresholds3);
        f3=vbslq_f32(e3, buckets3, f3);

        e3=vcgtq_f32(c3,thresholds4);
        f3=vbslq_f32(e3, buckets4, f3);


        vst1q_f32(&finalOutputArrayF[i], f0);
        vst1q_f32(&finalOutputArrayF[i+4], f1);
        vst1q_f32(&finalOutputArrayF[i+8], f2);
        vst1q_f32(&finalOutputArrayF[i+12], f3);
    }
}
NSLog(@"done");
}

PS: これはこの規模での私の最初のベンチマークなので、シンプルに保つようにしました (大きなループ、設定コード定数、NSlog を使用した開始/終了時間の出力、リンクされたフレームワークの高速化のみ)。これらの仮定のいずれかが結果に大きな影響を与えている場合は、批判してください。

ありがとう

4

3 に答える 3

6

まず、これは「ネオン」そのものではありません。これは組み込み関数です。clangまたはgccで組み込み関数を使用して良好なNEONパフォーマンスを得るのはほとんど不可能です。組み込み関数が必要だと思われる場合は、アセンブラを手書きする必要があります。

vDSPはNEONよりも「最適化」されていません。iOSのvDSPはNEONプロセッサを使用します。vDSPによるNEONの使用は、NEONの使用よりもはるかに最適化されています。

私はまだ組み込みコードを掘り下げていませんが、問題の最も可能性の高い(実際にはほぼ確実な)原因は、待機状態を作成していることです。アセンブラーでの書き込み(および組み込み関数は、溶接手袋を着用して作成されたアセンブラーです)は、Cでの書き込みとはまったく異なります。同じループを作成することはありません。あなたは同じものを比較しません。新しい考え方が必要です。アセンブリでは、一度に複数のことを実行できますが(ロジックユニットが異なるため)、これらすべてを並行して実行できるようにスケジュールを設定する必要があります。良好な組み立てにより、これらすべてのパイプラインがいっぱいになります。あなたがあなたのコードを読むことができて、それが完全に理にかなっているなら、それはおそらくくだらないアセンブリコードです。あなたが自分自身を繰り返さないのであれば、それはおそらくがらくたのアセンブリコードです。

Cを音訳するのと同じくらい簡単であれば、コンパイラがそれを行います。「これをNEONで書く」と言った瞬間、コンパイラも使っているので「コンパイラよりもいいNEONを書けると思う」と言っています。とは言うものの、コンパイラー(特にgccとclang)よりも優れたNEONを作成できることがよくあります。

あなたがその世界に飛び込む準備ができているなら(そしてそれはかなりクールな世界です)、あなたはあなたの前にいくつかの読書を持っています。ここに私がお勧めするいくつかの場所があります:

言ったことすべて...常に常にアルゴリズムを再考することから始めます。多くの場合、答えはループをすばやく計算する方法ではなく、ループをそれほど頻繁に呼び出さない方法です。

于 2013-02-18T01:48:26.273 に答える
4

ARM NEONには32個のレジスタ、64ビット幅があります(16個のレジスタ、128ビット幅としてのデュアルビュー)。あなたのネオンの実装はすでに少なくとも18個の128ビット幅を使用しているので、コンパイラはそれらをスタックから前後に移動するコードを生成しますが、それは良くありません-余分なメモリアクセスが多すぎます。

アセンブリで遊ぶことを計画している場合は、ツールを使用してオブジェクトファイルに命令をダンプするのが最善であることがわかりました。1つはobjdumpLinuxで呼ばれotool、Appleの世界で呼ばれていると思います。このようにして、結果のマシンコードがどのように見えるか、およびコンパイラが関数をどのように処理したかを実際に確認できます。

以下は、gcc(-O3)4.7.1からのネオン実装のダンプの一部です。を介してクワッドレジスタをロードしていることに気付くでしょうvldmia sp, {d8-d9}

1a6:    ff24 cee8   vcgt.f32    q6, q10, q12
1aa:    ff64 4ec8   vcgt.f32    q10, q10, q4
1ae:    ff2e a1dc   vbit    q5, q15, q6
1b2:    ff22 ceea   vcgt.f32    q6, q9, q13
1b6:    ff5c 41da   vbsl    q10, q14, q5
1ba:    ff20 aeea   vcgt.f32    q5, q8, q13
1be:    f942 4a8d   vst1.32 {d20-d21}, [r2]!
1c2:    ec9d 8b04   vldmia  sp, {d8-d9}
1c6:    ff62 4ee8   vcgt.f32    q10, q9, q12
1ca:    f942 6a8f   vst1.32 {d22-d23}, [r2]

もちろん、これはすべてコンパイラに依存します。より優れたコンパイラは、使用可能なレジスタをより明確に使用することで、この状況を回避できます。

したがって、アセンブリ(インライン、スタンドアロン)を使用しない場合、または必要なものが得られるまでコンパイラの出力を継続的にチェックする必要がある場合は、最終的にコンパイラに翻弄されます。

于 2013-02-18T07:54:41.810 に答える
2

ロブの答えを補完するものとして、NEONを書くことはそれ自体が芸術です(ちなみに、私のWandering Coderの投稿を差し込んでくれてありがとう)、およびauselenの答え(あなたは実際に常にあまりにも多くのレジスタをライブにしているということです。こぼれに)、あなたの組み込みアルゴリズムは他の2つよりも一般的であることを追加する必要があります.倍数だけでなく任意の範囲を許可するため、比較できないものを比較しようとしています. 常にオレンジとオレンジを比較してください。ただし、カスタムアルゴリズムの特定の機能のみが必要な場合は、カスタムアルゴリズムを既製の汎用アルゴリズムよりも具体的に比較することは公正なゲームです。これは、NEON アルゴリズムが C アルゴリズムと同じくらい遅くなる別の方法です: それらが同じアルゴリズムでない場合です。

ヒストグラム作成のニーズに関しては、vDSP で構築したものを当分の間のみ使用してくださいアプリケーションのパフォーマンスが満足のいくものでない場合にのみ、別の方法で最適化を検討してください。そうする手段としては、NEON 命令を使用する以外に、大量のメモリ移動 (vDSP 実装のボトルネックとなる可能性があります) を回避し、この中間出力を強制的に作成する代わりに、ピクセルを参照するときに各バケットのカウンターをインクリメントすることが含まれます。値。効率的な DSP コードは、計算自体だけでなく、メモリ帯域幅を最も効率的に使用する方法などにも関係します。モバイルではなおさらです。メモリ I/O は、キャッシュであっても、プロセッサ コア内の操作よりも電力を消費するため、両方のメモリ I/O バスがプロセッサ クロック速度の低い割合で実行される傾向があります。 、そのため、遊ぶためのメモリ帯域幅がそれほど多くありませんメモリ帯域幅を使用すると電力が消費されるため、メモリ帯域幅を賢く使用する必要があります。

于 2013-02-19T10:34:17.523 に答える