NEONコプロセッサの概要
Cortex-Aシリーズプロセッサに搭載されているSIMD演算(*1)を行う演算器で、マルチメディア演算(ビデオエンコード / デコード / 画像処理 / 音声処理など)や大量のデータを演算する場合、効率的に処理できます。
データ型 | 8ビット | 16ビット | 32ビット | 64ビット |
---|---|---|---|---|
符号なし整数 | U8 | U16 | U32 | U64 |
符号付き整数 | S8 | S16 | S32 | S64 |
型指定なし整数 | I8 | I16 | I32 | I64 |
浮動小数点 | — | F16 | F32 | — |
多項式 | P8 | P16 | — | — |
(*1)SIMD(single instruction multiple data)演算とは、1命令で複数データの演算を行うコンピュータの並列化の形態のことをいいます(パック演算 / パックド演算・ベクトル演算とも表現します)。
(*2)命令によって使用できないデータ型がありますので注意ください。
Armレジスタを使用した演算処理例
Armレジスタの演算は、1命令で1演算処理を行います。
【Armレジスタを使用した演算例】
NEONレジスタを使用した演算処理例
NEONレジスタの演算処理は、指定したデータサイズにて1命令で複数の演算処理を行います。演算処理は、指定したデータサイズごとに行われます。64ビット幅のレジスタを選択し、16ビット幅の演算を行う場合、0から15ビット、16から31ビット、32から47ビット、48から63ビットの各々で演算を行い、この単位をレーンと呼びます。
【NEONレジスタを使用した演算例】
NEONレジスタセット
64ビット幅の32個のレジスタ(d0からd31)または、128ビット幅の16個のレジスタ(q0からq15)として使用することができます。d0レジスタの内容を変更した場合、q0レジスタの下位64ビットが同一の内容になります。
NEONコプロセッサの初期化処理
NEONコプロセッサは、リセット時に無効化されるため、初期化処理でアクセス権設定と稼働設定が必要です。NEONコプロセッサが無効状態でNEON命令を実行した場合、「未定義命令例外」が発生します。
【NEONコプロセッサ稼働プログラム例】
;==================================================================== ; CP10/CP11のアクセス許可 ;==================================================================== MRC p15, 0, r0, c1, c0, 2 ; コプロセッサアクセス制御レジスタ(CPACR)リード ORR r0, r0, #(0xF << 20) ; CP10/11のフルアクセス設定 MCR p15, 0, r0, c1, c0, 2 ; コプロセッサアクセス制御レジスタ(CPACR)ライト ISB ;==================================================================== ; VFPをNEON動作開始 ;==================================================================== MOV r0, #0x40000000 ; VMSR FPEXC, r0 ; 浮動小数点例外レジスタでENビットを設定
NEONベクタ命令セット
NEONベクタ命令セットはアセンブリ命令がVで始まり、処理命令以外に演算結果のサイズやデータタイプを設定し、レジスタ幅とデータタイプで演算処理を行うレーン数が決まります。例えば、qレジスタ(128ビット)でデータサイズが8ビットの場合、16レーンとなり、同時に16回の演算処理を行うことができます。
V{<mod>}<op>{<shape>}{<cond>}{.<dt>} (<dest>},src1,src2
設定項目 | 設定内容 |
---|---|
命令修飾<mod> | Q:飽和算術演算を行います。(例:VQADD) |
H:結果を半分にします。(例:VHADD) | |
D:結果を2倍にします。(例:VQDMUL) | |
R:結果の丸めを行います。(例:VRHADD) | |
命令処理<op> | 操作(例: ADD、SUB、MULなど) |
<shape> | L:2つのオペランドは2倍のビット幅になります。 |
W:最後のオペランドは2倍のビット幅になります。 | |
N:結果は半分のビット幅になります。 | |
条件<cond> | 条件命令を実行します(Thumb2命令 ITブロックで使用されます)。 |
データタイプ<dt> | データ型を指定します。 符号なし整数、U8、U16、U32、U64 符号付き整数、S8、S16、S32、S64 型指定なしの整数、I8、I16、I32、I64 浮動小数点数値、F16、F32 多項式、P8、P16 |
dest | デスティネーション |
src1 | ソースオペランド1 |
src2 | ソースオペランド2 |
ArmレジスタからNEONレジスタへのデータ転送命令
ArmレジスタからNEONレジスタにデータ転送を行います。d0レジスタの下位32ビットにArmレジスタのr1レジスタの内容を転送し、d0レジスタの上位32ビットにArmレジスタのr0レジスタの内容を転送します。
VMOV d0,r0,r1 ; d0=r0(上位32ビット)+r1(下位32ビット)
Armレジスタの内容をNEONレジスタへのコピー命令
ArmレジスタをNEONレジスタに32ビット単位でコピーします。
VDPU.32 q0,r0 ; r0レジスタを32ビット毎にq0レジスタにコピー
加算命令
16ビットデータ×4の加算命令は、16ビットレーンごとに加算処理を行います。
VADD.I16 d2,d1,d0 ; d2 = d1 + d0(16ビットレーンごとに加算)
ロード命令
メモリからNEONレジスタに読み込みを行う場合、Armレジスタに読み込み開始アドレスを設定します。r0レジスタが示すアドレス(0x80000000番地)から32ビット単位でd0、d1、d2レジスタに読み込みを行い、[r0]に!を設定することで、r0レジスタ(読み込みアドレス)の更新が可能です(アドレス更新を行う場合、r0レジスタは、0x80000018になります)。
VLD1.32 {d0,d1,d2},[r0] ; r0レジスタが示すアドレスから読み込み
ストア命令
NEONレジスタをメモリに書き込みを行う場合、Armレジスタに読み込み開始アドレスを設定します。r0レジスタが示すアドレス(0x80000000番地)に32ビット単位でd0、d1、d2レジスタの値を書き込み、[r0]に!を設定することで、r0レジスタ(書き込みアドレス)の更新が可能です(アドレス更新を行う場合、r0レジスタは、0x80000018になります)。
VST1.32 {d0,d1,d2},[r0] ; r0レジスタが示すアドレスに書き込み
NEONコプロセッサプログラミング
NEONコプロセッサを使用する場合、既存プログラムを修正してNEONベクタ命令を生成(自動ベクトル化)する方法、NEON組み込み関数を使用する方法、アセンブリ言語でNEONベクタ命令の使用する方法の3種類から選択することができます。アセンブリ命令は、Armプロセッサのパイプラインを考慮したプログラミングが必要となり、コーディングが難しいため、自動ベクトル化とNEON組み込み関数の使用する方法をお勧めします。
Armコンパイラの自動ベクトル化
Armコンパイラは、コンパイラオプションの設定とC/C++ソースコードを変更することで、NEONベクタ命令を生成します。「--diag_warning=optimizations
」オプションを設定することで、最適化に関する診断メッセージが出力されます。
No | 設定項目 | 設定値 |
---|---|---|
1 | 最適化設定値 | 「-O2-Otime 」または「-O3 -Otime 」を設定します。 |
2 | NEONベクタ命令設定 | 「--vectorize 」を設定します。 |
3 | プロセッサ設定値 | NEONコプロセッサ搭載Armプロセッサを設定します。 例:「 --cpu Cortex-A9 」 |
No | 変更ポイント |
---|---|
1 | 行数の少ない単純なループにして下さい。 |
2 | ループからbreak文で抜けないで下さい。 |
3 | ループの回数を2nにして下さい。 |
4 | ループ回数が特定できるようにしてください。 |
5 | ループ内関数は、インライン化することをお勧めします。 |
6 | ポインタはインデックス[]を使用してください。 |
7 | メモリ空間がオーバーラップしないために、__restrict キーワード(*3)を使用します。 |
(*3)__restrict
とは、さまざまなオブジェクトのポインタ型や関数パラメータ配列が、重複するメモリ領域を使用しないことをコンパイラに指示する設定です。
自動ベクトル化例
float型配列の加算処理を自動ベクトル化できるようにソースコードを変更します。
【変更前ソースコード】
- コンパイラオプションに「
-O2 -Otime –vectorize –cpu=Cortex-A9
」を設定します。 - 上記【ソースコードの変更点】を参考にプログラムを修正します。
void float_add(float *fres,float *fdata1,float *fdata2) { unsigned long i; for(i=0;i<128;i++){ *fres = (*fdata1)+(*fdata2); fres++; fdata1++; fdata2++; } }
【変更後ソースコード】
次の変更を行うことで、ベクトル演算が可能となります。
- 引数のポインタ型に
__restrict
属性を設定します。 - ポインタアクセスからインデックスアクセスに変更します。
組み込み関数によるベクトル化
自動ベクトル化ができない場合、組み込み関数を使用してベクトル化します。2つのfloat型配列の合計値を求めるプログラムをNEON組み込関数でベクトル化します。
【変更前ソースコード】
本プログラムは、自動ベクトル化できません。筆者は、res変数で配列の合計値を求めているので、並列演算を行うことができないのでは?と考えています。そこで、NEON組み込み関数を使用してベクトル化します。
float calc(float * __restrict fdata1,float * __restrict fdata2) { float res=0.0; unsigned long i; for(i=0;i<128;i++){ res += fdata1[i]+fdata2[i]; } return res; }
【変更後ソースコード(*4)】
NEON組み込み関数でベクトル演算を行います。
arm_neon.h
ヘッダファイルをインクルード。- 単精度浮動小数演算(float型)を行うので、
float32x4_t
の変数を定義(NEONコプロセッサのqレジスタ(128ビット幅)として利用)。 - 演算回数は、1回で4回の演算を行うので、128/4=32回に変更。
vld1q_f32()
組み込み関数を使用して、配列の先頭から4個のfloat型データをqレジスタに読み込み。vaddq_f32()
組み込み関数を使用して、4個のfloat型変数の加算。vgetq_lane_f32()
組み込み関数で、レーン毎の合計値を求める。- ポインタを更新。
(*4)変更前ソースコードと演算の順番が異なりますので、演算精度の影響で同一の結果とならない場合があります。
まとめ
Cortex-Aシリーズには、NEONコプロセッサが搭載されているので、演算処理をSIMD演算に変更することで高速化が可能となるため、オープンソースの音声コーデックプログラムの自動ベクトル化やNEON組み込み関数を使用したチューニングを行いました。その結果、実行速度を向上することは簡単ではないことを実感することができました(逆に、性能が低下する問題が発生し困ったことがありました)。そこで、筆者が感じたNEONコプロセッサを有効に使うための考慮点をまとめてみました。
- 大量データの演算処理を行うためには、NEONコプロセッサが効率的に読み込み・書き込みができるデータ構造にすることが重要です(VLD/VST命令の動作の理解が必要です)。
- NEON組み込み関数では、多くの変数定義により使用できるNEONレジスタがなくなった場合、NEONレジスタをスタックに退避・復帰を行いますので、実行速度が低下します。
- 自動ベクトル化は、適切なソースコードに変更しない限り行われることはありません。既存ソースコードの多くは、SIMD演算を想定してプログラムは作成されてない場合が多いと考えます。
- PMU(第16回参照)を使用して実行時間を測定し、修正内容に対する実行時間の変化の把握が必要です。
あわせて読みたい
NEONコプロセッサは上手に使用すると、演算処理を効率化できますので、チャレンジしてみてはいかがでしょうか。
参考資料
NEONコプロセッサの使用法を学ぶためには、次の3種類のマニュアルを参照ください。
Cortex-A9 NEON Media Processing Engine Revision: r4p1 Technical Reference Manual
NEON Programmer's Guide Version: 1.0
Arm NEON Intrinsics Reference (Document number:IHI 0073A)
こちらも是非
“もっと見る” Cortex-A編
初期化処理
リセット例外からmain()関数を呼び出すまでの初期化は、ユーザが作成する部分とArmコンパイラが実行する部分に分けることができます。コードのコピーや初期化変数/未初期化変数の初期化は、リンカのメモリ配置設定を処理系ライブラリが実行します。
PMU(パフォーマンス監視ユニット)
PMUに関連するレジスタは、ユーザモードでのアクセスは禁止されていますので、PMUSERENR(ユーザイネーブルレジスタ)を特権モードでユーザモードアクセス許可を設定します。PMUSERENRについては、 後述の該当項目を参照ください。
TrustZone(セキュリティ拡張機能)
TrustZoneはCortex-Aシリーズの拡張機能で、大規模OSやアプリケーションが動作するノーマルワールドとセキュリティ関連が動作するセキュアワールドを導入しています。TrustZoneでは、ノーマルワールドメモリ空間とセキュアワールドメモリ空間の分離が可能です。