7

ARM NEON アセンブラーを使用して、4D (128 ビット) 行列ベクトル乗算の最適化に取り組んでいます。

行列とベクトルを NEON レジスタにロードして変換しても、パフォーマンスは大幅に向上しません。NEON レジスタへの切り替えには 20 サイクルかかるからです。さらに、変更されていないにもかかわらず、乗算ごとに行列をリロードします。

一度により多くのベクトルで変換を実行するのに十分なレジスタ空間があります。これにより、パフォーマンスが向上します。

しかし..

アセンブラー内ですべての頂点をループする (ポインターを増やす) と、この操作がどれだけ速くなるか疑問に思っています。しかし、私は Neon アセンブラーの最初にいますが、これを行う方法がわかりません。誰かが私に手を差し伸べることができますか?

私が達成したいこと:

  1. 行列と最初のベクトルをロードする
  2. ループカウント「count」を保存し、..
  3. -- LOOP_START --
  4. 乗加算を実行する (変換を行う)
  5. vOut に q0 を書き込む
  6. ポインター vIn および vOut を 4 (128 ビット) 増やします。
  7. vIn を q5 にロードします。
  8. -- LOOP_END --

既存の C バージョンのループ:

void TransformVertices(ESMatrix* m, GLfloat* vertices, GLfloat* normals, int count)
{
    GLfloat* pVertex = vertices;
    int i;  

    // iterate trough vertices only one at a time
    for (i = 0; i < count ; i ++)
    {
        Matrix4Vector4Mul( (float *)m, (float *)pVertex, (float *)pVertex);
        pVertex += 4;
    }

    //LoadMatrix( (const float*) m);

    //// two at a time
    //for (i = 0; i < count ; i += 2)
    //{
    //    Matrix4Vector4Mul2( (float *)m, (float *)pVertex, (float *)(pVertex + 4));
    //      pVertex += 8;
    //}
}

変換を 1 つだけ実行する場合の NEON-Version の次のコード:

void Matrix4Vector4Mul (const float* m, const float* vIn, float* vOut)
{    
    asm volatile
    (

    "vldmia %1, {q1-q4 }     \n\t"
    "vldmia %2, {q5}         \n\t"

    "vmul.f32 q0, q1, d10[0] \n\t"        
    "vmla.f32 q0, q2, d10[1] \n\t"      
    "vmla.f32 q0, q3, d11[0] \n\t"        
    "vmla.f32 q0, q4, d11[1] \n\t"

    "vstmia %0, {q0}"

    : // no output
    : "r" (vOut), "r" (m), "r" (vIn)       
    : "memory", "q0", "q1", "q2", "q3", "q4", "q5" 
    );

}

変換の C バージョン:

void Matrix4Vector4Mul (const float* m, const float* vIn, float* vOut)
{
    Vertex4D* v1 =    (Vertex4D*)vIn;
    Vertex4D vOut1;
    Vertex4D* l0;
    Vertex4D* l1;
    Vertex4D* l2;
    Vertex4D* l3;

    // 4x4 Matrix with members m00 - m33 
    ESMatrix* m1 = (ESMatrix*)m;

    l0 = (Vertex4D*)&m1->m00;
    vOut1.x = l0->x * v1->x;
    vOut1.y = l0->y * v1->x;
    vOut1.z = l0->z * v1->x;
    vOut1.w = l0->w * v1->x;

    l1 = (Vertex4D*)&m1->m10;
    vOut1.x += l1->x * v1->y;
    vOut1.y += l1->y * v1->y;
    vOut1.z += l1->z * v1->y;
    vOut1.w += l1->w * v1->y;

    l2 = (Vertex4D*)&m1->m20;
    vOut1.x += l2->x * v1->z;
    vOut1.y += l2->y * v1->z;
    vOut1.z += l2->z * v1->z;
    vOut1.w += l2->w * v1->z;

    l3 = (Vertex4D*)&m1->m30;
    vOut1.x += l3->x * v1->w;
    vOut1.y += l3->y * v1->w;
    vOut1.z += l3->z * v1->w;
    vOut1.w += l3->w * v1->w;

    *(vOut) = vOut1.x;
    *(vOut + 1) = vOut1.y;
    *(vOut + 2) = vOut1.z;
    *(vOut + 3) = vOut1.w;
}

パフォーマンス: (変換 > 90,000 頂点 | Android 4.0.4 SGS II)

C-Version:    190 FPS 
NEON-Version: 162 FPS ( .. slower -.- )

--- LOAD Matrix only ONCE (seperate ASM) and then perform two V's at a time ---

NEON-Version: 217 FPS ( + 33 % NEON | + 14 % C-Code )
4

3 に答える 3

1

コンパイラフラグで遊んでみましたか?

-mcpu=cortex-a9 -mtune=cortex-a9 -mfloat-abi=softfp -mfpu=neon -O3

この場合、私にとってはかなりの仕事をします(gcc 4.4.3、Android NDK 8bで配布)。内部関数 static および inline を定義し、行列 (m[X][0] など) を静的グローバル変数に移動することによって、タイトなソース コードを作成するか、Matrix4Vector4Mul をループにマージして、関数に渡し続ける代わりに行列ローカル変数を作成します。 - gcc はそこで賢くなりません。

これを行うと、メインループの下に移動します。

  a4:   ed567a03    vldr    s15, [r6, #-12]
  a8:   ee276aa0    vmul.f32    s12, s15, s1
  ac:   ee676aa8    vmul.f32    s13, s15, s17
  b0:   ed564a04    vldr    s9, [r6, #-16]
  b4:   ee277a88    vmul.f32    s14, s15, s16
  b8:   ed165a02    vldr    s10, [r6, #-8]
  bc:   ee677a80    vmul.f32    s15, s15, s0
  c0:   ed565a01    vldr    s11, [r6, #-4]
  c4:   e2833001    add r3, r3, #1
  c8:   ee046a89    vmla.f32    s12, s9, s18
  cc:   e1530004    cmp r3, r4
  d0:   ee446aaa    vmla.f32    s13, s9, s21
  d4:   ee047a8a    vmla.f32    s14, s9, s20
  d8:   ee447aa9    vmla.f32    s15, s9, s19
  dc:   ee056a22    vmla.f32    s12, s10, s5
  e0:   ee456a01    vmla.f32    s13, s10, s2
  e4:   ee057a21    vmla.f32    s14, s10, s3
  e8:   ee457a02    vmla.f32    s15, s10, s4
  ec:   ee056a8b    vmla.f32    s12, s11, s22
  f0:   ee456a83    vmla.f32    s13, s11, s6
  f4:   ee057aa3    vmla.f32    s14, s11, s7
  f8:   ee457a84    vmla.f32    s15, s11, s8
  fc:   ed066a01    vstr    s12, [r6, #-4]
 100:   ed466a04    vstr    s13, [r6, #-16]
 104:   ed067a03    vstr    s14, [r6, #-12]
 108:   ed467a02    vstr    s15, [r6, #-8]
 10c:   e2866010    add r6, r6, #16
 110:   1affffe3    bne a4 <TransformVertices+0xa4>

4 つのロード、4 つの乗算、12 の乗算と累算、4 つのストアがあり、Matrix4Vector4Mul で行っていることと一致します。

コンパイラで生成されたコードにまだ満足できない場合は、コンパイラの '-S' を渡してアセンブリ出力を取得し、それを出発点として使用して、ゼロから始めるのではなく、さらに改善してください。

verticesまた、適切なデータ フローを得るために、キャッシュ ライン サイズがアラインされている (Cortex-A9 の場合は 32 バイト)ことも確認する必要があります。

ベクトル化には、ベクトル化された情報を出力するなどの gcc オプションがあります-ftree-vectorizer-verbose=9また、gcc のドキュメントでこれを検索して、gccを指示する方法や、乗算をベクトル化するために何を変更する必要があるかを確認してください。これは掘り下げるには大変に聞こえるかもしれませんが、長い目で見れば、「手作業によるベクトル化」よりも実り多いものになるでしょう。

于 2012-10-21T00:15:56.697 に答える
0

手動で調整されたネオン バージョンは、すべての操作間の依存関係に悩まされていますが、gcc は c バージョンの順不同のスケジューリングを行うことができます。2 つ以上の独立したスレッドを並行して計算することで、NEON バージョンを改善できるはずです。

NEON でのポインタのインクリメント (ポスト インクリメント) は、エクスクラメーション マークで行われます。これらのレジスタは、出力レジスタ リスト "=r" (vOut) に含める必要があります。

vld1.32 {d0,d1}, [%2]!   ; // next round %2=%2 + 16 
vst1.32 {d0},    [%3]!   ; // next round %3=%3 + 8

別のアドレッシング モードでは、別のアーム レジスタで定義された「ストライド」によるポスト インクリメントが可能です。このオプションは、一部のロード コマンドでのみ使用できます (さまざまなインターリーブ オプションと、たとえば d1[1] (上部) の選択された要素へのロードがあるため)。

vld1.16 d0, [%2], %3    ; // increment by register %3

カウンターのインクリメントはシーケンスで発生します

1: subs %3, %3, #1      ; // with "=r" (count) as fourth argument
bne 1b                  ; // create a local label

同じファイルに 2 つの「bne loop」ステートメントがあるとエラーが発生するため、ローカル ラベルが使用されます。

単一要素の代わりにベクトルの融合乗算加算を計算することにより、並列処理を 4 倍に増やすことができるはずです。

この場合、事前に (ルーチンを呼び出す前に、または特別なアドレス指定モードで) 行列転置を実行する価値があります。

asm(
   "vld1.32 {d0[0],d2[0],d4[0],d6[0]}, [%0]! \n\t"
   "vld1.32 {d0[1],d2[1],d4[1],d6[1]}, [%0]! \n\t"
   "vld1.32 {d1[0],d3[0],d5[0],d7[0]}, [%0]! \n\t"
   "vld1.32 {d1[1],d3[1],d5[1],d7[1]}, [%0]! \n\t"

   "vld1.32 {q8}, [%2:128]! \n\t"
   "vld1.32 {q9}, [%2:128]! \n\t"
   "vld1.32 {q10}, [%2:128]! \n\t"
   "vld1.32 {q11}, [%2:128]! \n\t"

   "subs %0, %0, %0 \n\t"   // set zero flag

   "1: \n\t"
   "vst1.32 {q4}, [%1:128]! \n\t"
   "vmul.f32 q4, q8, q0 \n\t"
   "vst1.32 {q5}, [%1:128]! \n\t"
   "vmul.f32 q5, q9, q0 \n\t"
   "vst1.32 {q6}, [%1:128]! \n\t"
   "vmul.f32 q6, q10, q0 \n\t"
   "vst1.32 {q7}, [%1:128]!  \n\t"
   "vmul.f32 q7, q11, q0 \n\t"

   "subne %1,%1, #64    \n\t"    // revert writing pointer in 1st iteration 

   "vmla.f32 q4, q8, q1 \n\t"
   "vmla.f32 q5, q9, q1 \n\t"
   "vmla.f32 q6, q10, q1 \n\t"
   "vmla.f32 q7, q11, q1 \n\t"
   "subs %2, %2, #1 \n\t"
   "vmla.f32 q4, q8, q2 \n\t"
   "vmla.f32 q5, q9, q2 \n\t"
   "vmla.f32 q6, q10, q2 \n\t"
   "vmla.f32 q7, q11, q2 \n\t"

   "vmla.f32 q4, q8, q3 \n\t"
   "vld1.32 {q8}, [%2:128]! \n\t"  // start loading vectors immediately
   "vmla.f32 q5, q9, q3 \n\t"
   "vld1.32 {q9}, [%2:128]! \n\t"  // when all arithmetic is done
   "vmla.f32 q6, q10, q3 \n\t"
   "vld1.32 {q10}, [%2:128]! \n\t"
   "vmla.f32 q7, q11, q3 \n\t"
   "vld1.32 {q11}, [%2:128]! \n\t"
   "jnz b1 \n\t"
   "vst1.32 {q4,q5}, [%1:128]! \n\t"  // write after first loop
   "vst1.32 {q6,q7}, [%1:128]! \n\t"
 : "=r" (m), "=r" (vOut), "=r" (vIn), "=r" ( N ), 
 :
 : "d0","d1","q0", ... ); // marking q0 isn't enough for some gcc version 

128 ビットで整列されたブロックの読み取りと書き込み (データ ptr も整列されていることを確認して
ください) ptr=((int)ptr + 15) & ~15

結果を書き込むポスト ループ ブロックがあるのと同じように、vOut への意味のない最初の書き込みをスキップする同様のプレ ループ ブロックを記述することができます (これは、条件付き書き込みによって克服することもできます)。残念ながら、条件付きで 64 ビット レジスタしか書き込むことができません。

于 2012-10-19T15:32:21.927 に答える
0

これは今ではほぼ 1 年前のトピックですが、「正しい」回答を提供することが重要だと思います。

  1. q4-q7 は使用前に保存する必要があるため、可能であれば使用を避ける必要があります。

  2. これについて間違っている場合は訂正してください。ただし、記憶に問題がない場合は、d0 ~ d3 (または d0 ~ d7) のみがスカラーを保持できます。gcc が d10 と d11 をスカラー オペランドとして許容している理由が本当に不思議です。そのようにすることは物理的に不可能なので、gcc はインライン アセンブリで再びおかしなことをしていると思います。インライン アセンブリ コードの逆アセンブリを確認します。

確かに、インライン アセンブリ コードには 2 つのインターロック (ロード後 2 サイクル、ストア前 9 サイクル) がありますが、NEON コードが C コードよりも遅く実行されることは想像できません。

私の側からは、gcc がエラー メッセージを吐き出す代わりに、大量のレジスタを行ったり来たりしているというのは非常に強い推測です。そして、この場合、それは正確には有利ではありません。

于 2013-08-29T07:56:10.640 に答える