31

C++コードは次のとおりです。

#define ARR_SIZE_TEST ( 8 * 1024 * 1024 )

void cpp_tst_add( unsigned* x, unsigned* y )
{
    for ( register int i = 0; i < ARR_SIZE_TEST; ++i )
    {
        x[ i ] = x[ i ] + y[ i ];
    }
}

これがネオンバージョンです:

void neon_assm_tst_add( unsigned* x, unsigned* y )
{
    register unsigned i = ARR_SIZE_TEST >> 2;

    __asm__ __volatile__
    (
        ".loop1:                            \n\t"

        "vld1.32   {q0}, [%[x]]             \n\t"
        "vld1.32   {q1}, [%[y]]!            \n\t"

        "vadd.i32  q0 ,q0, q1               \n\t"
        "vst1.32   {q0}, [%[x]]!            \n\t"

        "subs     %[i], %[i], $1            \n\t"
        "bne      .loop1                    \n\t"

        : [x]"+r"(x), [y]"+r"(y), [i]"+r"(i)
        :
        : "memory"
    );
}

テスト機能:

void bench_simple_types_test( )
{
    unsigned* a = new unsigned [ ARR_SIZE_TEST ];
    unsigned* b = new unsigned [ ARR_SIZE_TEST ];

    neon_tst_add( a, b );
    neon_assm_tst_add( a, b );
}

私は両方の亜種をテストしました、そしてここにレポートがあります:

add, unsigned, C++       : 176 ms
add, unsigned, neon asm  : 185 ms // SLOW!!!

他のタイプもテストしました:

add, float,    C++       : 571 ms
add, float,    neon asm  : 184 ms // FASTER X3!

質問:32ビット整数型ではネオンが遅いのはなぜですか?

AndroidNDK用のGCCの最新バージョンを使用しました。NEON最適化フラグがオンになりました。分解されたC++バージョンは次のとおりです。

                 MOVS            R3, #0
                 PUSH            {R4}

 loc_8
                 LDR             R4, [R0,R3]
                 LDR             R2, [R1,R3]
                 ADDS            R2, R4, R2
                 STR             R2, [R0,R3]
                 ADDS            R3, #4
                 CMP.W           R3, #0x2000000
                 BNE             loc_8
                 POP             {R4}
                 BX              LR

ネオンの分解バージョンは次のとおりです。

                 MOV.W           R3, #0x200000
.loop1
                 VLD1.32         {D0-D1}, [R0]
                 VLD1.32         {D2-D3}, [R1]!
                 VADD.I32        Q0, Q0, Q1
                 VST1.32         {D0-D1}, [R0]!
                 SUBS            R3, #1
                 BNE             .loop1
                 BX              LR

これがすべてのベンチテストです:

add, char,     C++       : 83  ms
add, char,     neon asm  : 46  ms FASTER x2

add, short,    C++       : 114 ms
add, short,    neon asm  : 92  ms FASTER x1.25

add, unsigned, C++       : 176 ms
add, unsigned, neon asm  : 184 ms SLOWER!!!

add, float,    C++       : 571 ms
add, float,    neon asm  : 184 ms FASTER x3

add, double,   C++       : 533 ms
add, double,   neon asm  : 420 ms FASTER x1.25

質問:32ビット整数型ではネオンが遅いのはなぜですか?

4

5 に答える 5

48

Cortex-A8のNEONパイプラインは順番に実行され、ヒットアンダーミス(名前の変更なし)が制限されているため、メモリレイテンシによって制限されます(L1 / L2を超えるキャッシュサイズを使用しているため)。コードはメモリからロードされた値に即座に依存するため、メモリを常に待機して停止します。これは、NEONコードが非NEONよりもわずかに(わずかに)遅い理由を説明します。

アセンブリループを展開し、負荷と使用の間の距離を増やす必要があります。例:

vld1.32   {q0}, [%[x]]!
vld1.32   {q1}, [%[y]]!
vld1.32   {q2}, [%[x]]!
vld1.32   {q3}, [%[y]]!
vadd.i32  q0 ,q0, q1
vadd.i32  q2 ,q2, q3
...

ネオンレジスターがたくさんあるので、たくさん展開できます。整数コードでも同じ問題が発生しますが、A8整数の方がストールするよりもヒットアンダーミスの方が優れているため、それほどではありません。ボトルネックは、L1/L2キャッシュと比較して非常に大きいベンチマークのメモリ帯域幅/遅延になります。データが完全にL1またはL2、あるいはその両方にキャッシュされている場合の影響を確認するために、より小さなサイズ(4KB..256KB)でベンチマークを実行することもできます。

于 2011-04-20T17:07:39.660 に答える
17

この場合、メインメモリへの遅延によって制限されますが、NEONバージョンがASMバージョンよりも遅いことは明確ではありません。

ここでサイクル計算機を使用する:

http://pulsar.webshaker.net/ccc/result.php?lng=en

キャッシュミスペナルティの前に、コードは7サイクルかかる必要があります。調整されていないロードを使用しているため、および追加とストアの間の遅延のため、予想よりも遅くなります。

一方、コンパイラによって生成されたループには6サイクルかかります(一般的に、スケジュールも最適化もされていません)。しかし、それは4分の1の仕事をしています。

スクリプトからのサイクルカウントは完全ではないかもしれませんが、露骨に間違っているように見えるものは見当たらないので、少なくとも近いと思います。フェッチ帯域幅を最大にすると(ループが64ビットアラインされていない場合も)、ブランチで余分なサイクルが発生する可能性がありますが、この場合、それを隠すためのストールがたくさんあります。

答えは、Cortex-A8の整数には、レイテンシーを隠す機会が多いということではありません。実際、NEONのパイプラインと発行キューがずらされているため、通常は少なくなります。もちろん、これはCortex-A8でのみ当てはまります。Cortex-A9では状況が逆転する可能性があります(NEONは整数と並行して順番にディスパッチされますが、整数には順不同の機能があります)。このCortex-A8にタグを付けたので、それが使用していると思います。

これはさらなる調査を要求します。これが発生する可能性がある理由は次のとおりです。

  • 配列にどのような種類のアラインメントも指定していません。newが8バイトにアラインすることを期待していますが、16バイトにアラインしない可能性があります。16バイトに整列されていない配列を実際に取得しているとしましょう。次に、キャッシュアクセスで行間を分割することになり、追加のペナルティが発生する可能性があります(特にミスの場合)
  • キャッシュミスはストアの直後に発生します。Cortex-A8にメモリの明確化があるとは思わないため、ロードがストアと同じ行からのものである可能性があると想定する必要があります。したがって、L2の欠落したロードが発生する前に、書き込みバッファをドレインする必要があります。NEONロード(整数パイプラインで開始される)とストア(NEONパイプラインの最後で開始される)の間のパイプライン距離は整数のものよりもはるかに大きいため、ストールが長くなる可能性があります。
  • アクセスごとに4バイトではなく16バイトをロードしているため、クリティカルワードのサイズが大きくなり、メインメモリからのクリティカルワードの最初のラインフィルの実効レイテンシが高くなります(L2からL1は128ビットバス上にあるので、同じ問題は発生しないはずです)

このような場合にNEONがどのように優れているかを尋ねましたが、実際には、メモリとの間でストリーミングを行うこれらの場合にNEONは特に適しています。秘訣は、メインメモリのレイテンシを可能な限り隠すためにプリロードを使用する必要があるということです。プリロードは、事前にメモリをL2(L1ではなく)キャッシュに取り込みます。ここで、NEONは、パイプラインと発行キューがずらされているだけでなく、NEONへの直接パスがあるため、L2キャッシュのレイテンシーの多くを隠すことができるため、整数よりも大きな利点があります。依存関係が少なく、ロードキューを使い果たしていない場合は、有効なL2レイテンシが0〜6サイクル以下になると予想されますが、整数では、回避できない良好な最大16サイクルでスタックする可能性があります(おそらくただし、Cortex-A8に依存します)。

したがって、配列をキャッシュラインサイズ(64バイト)に調整し、ループを展開して一度に少なくとも1つのキャッシュラインを実行し、調整されたロード/ストアを使用して(アドレスの後に:128を配置)、複数のキャッシュラインをロードするpld命令。何行離れているかについては、小さく始めて、メリットがなくなるまで増やし続けます。

于 2011-05-30T17:36:49.443 に答える
15

C++コードも最適化されていません。

#define ARR_SIZE_TEST ( 8 * 1024 * 1024 )

void cpp_tst_add( unsigned* x, unsigned* y )
{
    unsigned int i = ARR_SIZE_TEST;
    do
    {
        *x++ += *y++;
    } (while --i);
}

このバージョンは、2サイクル/反復を消費しません。

その上、あなたのベンチマーク結果は私をまったく驚かせません。

32ビット:

この機能はNEONには単純すぎます。最適化の余地を残している十分な算術演算がありません。

はい、それは非常に単純なので、C ++バージョンとNEONバージョンの両方がほぼ毎回パイプラインの危険にさらされており、二重発行機能の恩恵を受ける可能性はほとんどありません。

NEONバージョンは、一度に4つの整数を処理することでメリットが得られる可能性がありますが、あらゆる危険からもはるかに苦しみます。それで全部です。

8ビット:

ARMは、メモリからの各バイトの読み取りが非常に遅いです。つまり、NEONは32ビットと同じ特性を示しますが、ARMは大幅に遅れています。

16ビット:ここでも同じです。ARMの16ビット読み取りを除いて、それほど悪くはありません。

float:C++バージョンはVFPコードにコンパイルされます。また、Coretex A8には完全なVFPはありませんが、VFPliteはパイプラインを使用しません。

NEONが32ビットを奇妙に処理しているわけではありません。理想的な条件を満たすのはARMだけです。あなたの関数はその単純さのためにベンチマークの目的には非常に不適切です。YUV-RGB変換のようなもっと複雑なものを試してください:

参考までに、完全に最適化されたNEONバージョンは、完全に最適化されたCバージョンの約20倍、完全に最適化されたARMアセンブリバージョンの8倍の速度で実行されます。それがあなたにNEONがどれほど強力であるかについてのいくつかのアイデアを与えることを願っています。

最後になりましたが、ARM命令PLDはNEONの親友です。適切に配置すると、パフォーマンスが少なくとも40%向上します。

于 2011-11-02T13:02:24.257 に答える
5

コードを改善するためにいくつかの変更を試すことができます。

可能な場合:-3番目のバッファーを使用して結果を保存します。-8バイトでデータを整列させてみてください。

コードは次のようになります(gccインライン構文がわかりません)

.loop1:
 vld1.32   {q0}, [%[x]:128]!
 vld1.32   {q1}, [%[y]:128]!
 vadd.i32  q0 ,q0, q1
 vst1.32   {q0}, [%[z]:128]!
 subs     %[i], %[i], $1
bne      .loop1

Exophaseが言うように、パイプラインのレイテンシーがあります。あなたが試すことができるかもしれません

vld1.32   {q0}, [%[x]:128]
vld1.32   {q1}, [%[y]:128]!

sub     %[i], %[i], $1

.loop1:
vadd.i32  q2 ,q0, q1

vld1.32   {q0}, [%[x]:128]
vld1.32   {q1}, [%[y]:128]!

vst1.32   {q2}, [%[z]:128]!
subs     %[i], %[i], $1
bne      .loop1

vadd.i32  q2 ,q0, q1
vst1.32   {q2}, [%[z]:128]!

最後に、メモリ帯域幅を飽和させることは明らかです

あなたは小さなを追加しようとすることができます

PLD [%[x], 192]

あなたのループに。

それが良いかどうか教えてください...

于 2011-06-07T07:19:40.370 に答える
0

8ミリ秒の差は非常に小さいため、キャッシュまたはパイプラインのアーティファクトを測定している可能性があります。

編集:フロートやショートなどのタイプについて、このようなものと比較してみましたか?コンパイラーがそれをさらに最適化し、ギャップを狭めることを期待しています。また、テストでは、最初にC ++バージョンを実行してからASMバージョンを実行します。これはパフォーマンスに影響を与える可能性があるため、より公平にするために2つの異なるプログラムを作成します。

for ( register int i = 0; i < ARR_SIZE_TEST/4; ++i )
{
    x[ i ] = x[ i ] + y[ i ];
    x[ i+1 ] = x[ i+1 ] + y[ i+1 ];
    x[ i+2 ] = x[ i+2 ] + y[ i+2 ];
    x[ i+3 ] = x[ i+3 ] + y[ i+3 ];
}

最後に、関数のシグネチャでは、のunsigned*代わりにを使用しますunsigned[]。コンパイラは配列がオーバーラップしていないと想定し、アクセスを並べ替えることができるため、後者が推奨されます。restrictエイリアシングに対する保護をさらに強化するためにも、キーワードを使用してみてください。

于 2011-04-20T16:52:27.267 に答える