4

NEONを使用してiPhoneARMの特定の問題に対してガウスニュートン最適化を実装しようとしています。以下の最初の関数は、私の元のC関数です。2つ目は、私が書いたNEONasmコードです。私はそれぞれを100,000回実行しましたが、NEONバージョンはCバージョンよりも7〜8倍長くかかります。読み込み(vld1.32)がほとんどの時間かかると思います。いくつかの指示を削除して実験しました。

誰かがこの問題について何か洞察を持っていますか?ありがとう!

template<class T>
inline void GaussNewtonOperationJtr8x8(T Jtr[8], const T J[8], T residual)
{
    Jtr[0] -= J[0]*residual;
    Jtr[1] -= J[1]*residual;
    Jtr[2] -= J[2]*residual;
    Jtr[3] -= J[3]*residual;
    Jtr[4] -= J[4]*residual;
    Jtr[5] -= J[5]*residual;
    Jtr[6] -= J[6]*residual;
    Jtr[7] -= J[7]*residual;    
}

inline void GaussNewtonOperationJtr8x8_NEON(NFloat Jtr[8], const NFloat J[8], NFloat residual)
{
    __asm__ volatile (
                      // load Jtr into registers
                      "vld1.32   {d0-d3}, [%0]\n\t"
                      // load J into registers
                      "vld1.32   {d4-d7}, [%1]\n\t"
                      // load residual in register
                      "vmov.f32  s16, %2\n\t"
                      // Jtr -= J*residual
                      "vmls.f32  q0, q2, d8[0]\n\t"
                      "vmls.f32  q1, q3, d8[0]\n\t"
                      // store result
                      "vst1.32   {d0-d3}, [%0]\n\t"
                      // output
                      :
                      // input
                      : "r"(Jtr), "r"(J), "r"(residual)
                      // registers
                      : "d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7", "d8", "d9", "d10", "d11", "d12", "d13", "d14"
                      );
}
4

4 に答える 4

6
  1. d8-d15は使用しないでください。それらは、使用する前にスタックに保存する必要があります。そして、後で復元されました。コンパイラーはこれを行う命令を出し、貴重なサイクルを無駄にします。
  2. Jtrの前にJをロードします。Jtrは、Jよりも後のパイプラインステージで期待されます。
  3. VLD/VSTの代わりにVLDMIA/VSTMIAを使用してください。VLDMIA / VSTMIAはより高速で、パイプラインに関して利点があります。
  4. ベクトル-スカラー乗算の代わりにベクトル-ベクトル乗算を使用します。
  5. ループバージョンを作成する場合は、pldを先頭に置き、ループを展開して、反復ごとに各ポインターから64バイトが読み取られるようにします。

私が上で述べたこれらの欠点のほかに-これはNEONに不慣れな人々に典型的です-あなたのアプローチはとても素晴らしいです。あなたはvmlsで最も適切な命令を見つけました。

素晴らしい。

{{

__asm__ volatile (
    // load residual in register
    "vdup.32  q12, %2\n\t"
    // load J into registers
    "vldmia   %1, {q10-q11}\n\t"
     // load Jtr into registers
    "vldmia   %0, {q8-q9}\n\t"
    // Jtr -= J*residual
    "vmls.f32  q8, q10, q12\n\t"
    "vmls.f32  q9, q11, q12\n\t"
    // store result
    "vstmia   %0, {q8-q9}\n\t"
    // output
    :
    // input
    : "r"(Jtr), "r"(J), "r"(residual)
    // registers
    : "q8", "q9", "q10", "q11", "q12"
);
于 2011-11-04T18:17:51.523 に答える
3

コンパイラ自体が、Cコードによって生成されたアセンブリを最適化します。あるコードを別のコードに変換するだけではありません。

あなたがやろうとしていることは、コンパイラよりも優れた最適化を行うことです(ああ)。少なくとも、コンパイラが上記のCコード用に生成しているアセンブリコードを知っていますか?アセンブリコードを改善したい場合は、そうする必要があります。

編集:

このスレッドには、この種のものについての素晴らしい議論があります。 なぜARMNEONはプレーンC++よりも高速ではないのですか?

于 2011-05-17T17:57:26.980 に答える
3

NEON命令とVFP命令を切り替えています。Cortex-A8とA9の両方でそうすることにはペナルティがあります。そのVFPvmov.f32命令を取り除き、パイプラインコンテキストスイッチを正当化するためにそのようなコードが長時間実行されない限り、このコードがVFPコードを使用する場所にインライン化されないようにしてください。

于 2011-05-18T10:06:00.813 に答える
1

あなたのC++バージョンは実際にfloatを使用していますか?テンプレートを提供しただけで、使用したインスタンス化が表示されなかったため、わかりません。このコードでは、NEONがCortex-A8のVFPよりも大幅に遅くなるのは非常に奇妙ですが、u32の場合は、そのように機能する可能性があります。

ABIが何であるかはわかりませんが、残差がどのように渡されるか(つまり、コンパイラーがそれをその%2レジスターに入れるために何をしているのか)にオーバーヘッドが生じる可能性があります。代わりにポインタを使用して、単一要素でvld1を使用してみてください。この方法でNEONにフロートを1つだけロードできます。

16バイトに整列されたロードとストアを使用すると、配列のパフォーマンスが向上しますが、入力をこのように機能させるには、いくつかのゲームをプレイする必要がある場合があります。残念ながら、長いvmls命令のレイテンシーのほとんどを回避していないため、これから本当に優れたパフォーマンスを得ることができません(NEON乗算をチェーンし、パイプラインをエンドツーエンドで追加するため)。NEONパイプラインの早い段階で入力を必要とするストアである依存命令のため、さらに悪いことになります。理想的には、これらの操作のいくつかを一度に実行でき、複数のインスタンスを一緒にインターリーブできます-レジスターに収まる限り多くのインスタンスをインターリーブできます。

于 2011-05-30T17:57:15.703 に答える