web-dev-qa-db-ja.com

ARM NEONのコーディング:開始方法?

バックグラウンド(必要に応じてこれをスキップ)

まず、私はエキスパートプログラマではありません。私は若いジュニアコンピュータービジョン(CV)エンジニアで、主に素晴らしいOpenCV2 C++ APIを広範囲に使用しているため、C++プログラミングでかなりの経験があります。私が学んだことは、プロジェクトを実行する必要性、問題を解決して期限を守る必要性、それが業界の現実であることだけでした。

最近、組み込みシステム(ARMボード)向けのCVソフトウェアの開発を開始し、プレーンなC++最適化コードを使用して開発しました。ただし、従来のコンピューターと比較してリソースが限られているため、この種のアーキテクチャーでリアルタイムCVシステムを構築することは大きな課題です。

NEONについて見つけたときのことです。私はこれについてたくさんの記事を読みましたが、これはかなり最近のテーマなので、それについての情報はあまりなく、読むほど、混乱します。

質問

一度に4つまたは8つの配列要素を計算するNEON機能を使用して、C++コード(主にforループ)を最適化しようとしています。 C++環境で使用できるライブラリまたは関数のセットはありますか?私の混乱の主な原因は、私が目にするほとんどすべてのコードスニペットがアセンブリにあるという事実です。そのため、私は背景がまったくなく、現時点では学ぶ余裕がありません。 Linux GentooでEclipse IDEを使用してC++コードを記述しています。

[〜#〜]更新[〜#〜]

回答を読んだ後、ソフトウェアでいくつかのテストを行いました。次のフラグを使用してプロジェクトをコンパイルしました。

-O3 -mcpu=cortex-a9 -ftree-vectorize -mfloat-abi=hard -mfpu=neon 

このプロジェクトには、openframeworks、OpenCV、OpenNIなどの広範なライブラリが含まれており、すべてがこれらのフラグを使用してコンパイルされていることに注意してください。 ARMボード用にコンパイルするには、Linaroツールチェーンクロスコンパイラを使用し、GCCのバージョンは4.8.3です。これによりプロジェクトのパフォーマンスが向上すると思いますか?まったく変更がなかったため、私がここで読んだすべての答えを考えると、これはかなり奇妙です。

別の質問:すべてのforサイクルには明らかな数の反復がありますが、それらの多くはカスタムデータ型(構造体またはクラス)を反復します。 GCCは、カスタムデータ型を反復処理しても、これらのサイクルを最適化できますか?

16
Pedro Batista

編集:

アップデートから、NEONプロセッサの動作を誤解する可能性があります。これは、SIMD(単一命令、複数データ)ベクトルプロセッサです。つまり、複数のデータに対して同時に命令(たとえば「4で乗算」)を実行するのは非常に優れています。また、「これらすべての数値をすべて加算する」、または「これらの2つの数値リストの各要素を追加して、3つ目の数値リストを作成する」などの操作も大好きです。したがって、問題がこれらの問題のように見える場合、NEONプロセッサが非常に役立ちます。

その利点を得るには、ベクトルプロセッサが複数のデータを同時にロードし、並列に処理して、同時に書き戻すことができるように、データを非常に特定の形式で配置する必要があります。数学がほとんどの条件を回避するように整理する必要があります(結果をすぐに見るとNEONへの往復が発生するため)。ベクトルプログラミングは、プログラムについての別の考え方です。パイプライン管理がすべてです。

現在、非常に一般的な種類の問題の多くは、コンパイラーが自動的にこれをすべて処理できます。しかし、それはまだ数字と特定の形式の数字を扱うことについてです。たとえば、ほとんどの場合、すべての数値をメモリ内の連続したブロックに入れる必要があります。構造体およびクラス内のフィールドを処理している場合、NEONは実際には役立ちません。これは、汎用の「並列処理」エンジンではありません。並列計算を行うためのSIMDプロセッサです。

非常に高性能なシステムでは、データ形式がすべてです。任意のデータ形式(構造体、クラスなど)を使用せず、それらを高速化しようとします。最も並列処理が可能なデータ形式を見つけ、それを中心にコードを記述します。データを隣接させます。すべてのコストでメモリ割り当てを回避します。しかし、これは単純なStackOverflowの質問で対処できるものではありません。高性能プログラミングは、スキルセット全体であり、物事についての異なる考え方です。これは、適切なコンパイラフラグを見つけることによって得られるものではありません。あなたが見つけたように、デフォルトはすでにかなり良いです。

OpenCVをより多く使用できるようにデータを再編成できるかどうかは、あなたが尋ねるべき真の質問です。 OpenCVにはすでにNEONを十分に活用できる最適化された並列処理がたくさんあります。できる限り、OpenCVが機能する形式でデータを保持する必要があります。これが、最大の改善点になる可能性が高い場所です。


私の経験では、clangやgccに勝るNEONアセンブリを手書きで書くことは確かに可能です(少なくとも数年前からですが、コンパイラは確実に改善を続けています)。優れたARM最適化はNEON最適化と同じではありません。@ Matsが指摘するように、コンパイラは通常、明らかなケースで優れた仕事をしますが、常にすべてのケースを理想的に処理するわけではありません。確かに、あまり熟練していない開発者でも、時には劇的に、時には劇的にそれを打つことができます(@wallykは、ハンドチューニングアセンブリが最後に保存されるのが最善ですが、それでも非常に強力な場合もあります)。

とは言っても、「私には背景がまったくなく、現時点では学ぶ余裕がないアセンブリ」というあなたの発言を踏まえると、いや、あなたも気にすべきではありません。少なくともアセンブリ(および特にベクトル化されたNEONアセンブリ)の基本(およびいくつかの非基本)を最初に理解していなければ、コンパイラを2番目に推測しても意味がありません。コンパイラーを打つステップ1は、ターゲットを認識しています。

ターゲットを学ぶ気があるなら、私のお気に入りの紹介は ARMアセンブリ)のWhirlwindツアー です。これに加えて、いくつかの他の参照(以下)で十分です。私の特定の問題ではコンパイラを2〜3倍上回りました。一方、経験豊富なNEON開発者にコードを見せたところ、彼はそれを約3秒間見ただけで十分でした。 。 "本当に良いアセンブリは難しいですが、半分まともなアセンブリは、最適化されたC++よりも優れている場合があります(これも、コンパイラの作成者が向上するにつれて、これは真実でなくなりますが、それでも真実である可能性があります)。

片側の注意点、 NEON組み込み関数での私の経験 は、それらが問題を起こす価値はほとんどないということです。コンパイラに勝つ場合は、実際に完全なアセンブリを作成する必要があります。ほとんどの場合、使用する組み込み関数が何であれ、コンパイラーはすでに知っていました。パイプラインを最適に管理するためにループを再構築することで、より多くの場合、力を得ることができます(そして、組み込み関数はそこで役立ちません)。これは過去2年間で改善された可能性がありますが、改善されたベクトルオプティマイザが逆の方法よりも組み込み関数の値を上回ることが期待されます。

14
Rob Napier

これは、ARMのブログ投稿を含む "mee too"です。 [〜#〜]最初[〜#〜]、以下から始めて背景を取得します32ビットARM(ARMV7以下)、Aarch32(ARMv8 32ビットARM)、Aarch64(ARMv8 64ビットARM))を含む情報:

2番目、チェックアウトNEONシリーズのコーディング。インターリーブされたロードのようなものが一目で理にかなっているので、写真付きの素晴らしい紹介です。

また、ARM NEONの扱いのあるアセンブリについての本を探してアマゾンに行きました。NEONの扱いのあるアセンブリは2つしか見つかりませんでした。どちらの本のNEONの扱いも印象的ではありませんでした。必須のマトリックスの例。


ARM組み込み関数は非常に良いアイデアです。組み込み関数を使用すると、GCC、Clang、およびVisual C/C++コンパイラのコードを記述できます。ARM Linuxディストリビューション(Linaroなど)、一部のiOSデバイス(-Arch armv7を使用)、およびMicrosoftガジェット(Windows PhoneやWindows Store Appsなど)。

7
jww

かなり現代的なGCC(GCC 4.8以降)にアクセスできる場合は、組み込み関数を試してみることをお勧めします。 NEON組み込み関数は、コンパイラーが認識している関数のセットであり、CまたはC++プログラムから使用してNEON/Advanced SIMD命令を生成できます。プログラムでそれらにアクセスするには、#include <arm_neon.h>。利用可能なすべての組み込み関数の詳細なドキュメントは http://infocenter.arm.com/help/topic/com.arm.doc.ihi0073a/IHI0073A_arm_neon_intrinsics_ref.pdf にありますが、より多くのユーザーが見つかります他のオンラインのフレンドリーなチュートリアル。

このサイトに関するアドバイスは、一般的にNEON組み込みに反するものであり、確かにそれらを実装するのに不十分なGCCバージョンがありますが、最近のバージョンはかなりうまくいきます(そして、悪いコード生成を見つけたら、バグとして報告してください- https://gcc.gnu.org/bugzilla/

これらはNEON/Advanced SIMD命令セットにプログラムする簡単な方法であり、多くの場合、達成できるパフォーマンスはかなり良好です。これらは「移植性」もあり、AArch64システムに移行すると、ARMv7-Aから使用できる組み込み関数のスーパーセットを利用できます。また、これらは、ARMアーキテクチャの実装間で移植可能です。これは、パフォーマンス特性が異なる場合がありますが、コンパイラーがパフォーマンス調整のためにモデル化します。

手書きのアセンブリに対するNEON組み込み関数の主な利点は、コンパイラがさまざまな最適化パスを実行するときにそれらを理解できることです。対照的に、手書きのアセンブラはGCCに対して不透明なブロックであり、最適化されません。一方、熟練したアセンブラープログラマーは、特に複数の連続したレジスターに書き込みまたは読み取りを行う命令を使用する場合に、コンパイラーのレジスター割り当てポリシーに打ち勝つことがよくあります。

5

Wallyの回答に加えて-おそらくコメントにする必要がありますが、十分に短くすることはできませんでした:ARMには、GCCの一部を改善することを全体的な役割とするコンパイラ開発者のチームがいます。 ARM CPUのコード生成を行うClang/llvm、「自動ベクトル化」を提供する機能を含む-私はそれについて詳しく調べていませんが、x86コード生成の経験から、ベクトル化が比較的容易なものを期待する場合、コンパイラーは適切な作業を行う必要があります。一部のコードは、ベクトル化できるかどうかをコンパイラーが理解するのが難しく、ループのアンロールや条件のマーク付けなどの「推奨」が必要になる場合があります。 「可能性が高い」または「可能性が低い」など.

免責事項:私はARMで働いていますが、グラフィックスを行うグループ(GPUドライバーのOpenCL部分にあるGPUのコンパイラーにある程度関わっています)で働いているため、コンパイラーやCPUとはほとんど関係がありません。

編集:

パフォーマンス、およびさまざまな命令拡張の使用は、実際にはコードが何をしているかに完全に依存しています。 OpenCVなどのライブラリは、コード内で既にかなりの機能(コンパイラ組み込み関数としての手書きアセンブラ、およびコンパイラがすでに適切に機能するように設計されているコードなど)を実行していると思います。本当にあなたに多くの改善を与えないかもしれません。私はコンピュータビジョンの専門家ではないので、OpenCVでこのような作業がどの程度行われているかについて正確にコメントすることはできませんが、コードの「最もホットな」ポイントがかなり適切に最適化されていることは確かです。

また、アプリケーションのプロファイルを作成します。最適化フラグをいじるだけでなく、そのパフォーマンスを測定し、プロファイリングツール(Linuxの「perf」ツールなど)を使用して、コードが時間を費やしている場所を測定します。次に、その特定のコードに対して何ができるかを確認します。それのより並列なバージョンを書くことは可能ですか?コンパイラは役に立ちますか?アセンブラを書く必要がありますか?同じことをするがより良い方法などで異なるアルゴリズムはありますか?.

コンパイラー・オプションを微調整すると役立つ場合があり、多くの場合は効果がありますが、アルゴリズムを変更すると、コードが10倍または100倍高速になることがよくあります。もちろん、アルゴリズムは改善できると想定しています!

ただし、アプリケーションのどの部分に時間がかかっているかを理解することが重要です。 5%の時間を費やすコードを10%速くするように変更しても意味がありません。他の場所で変更すると、合計時間の30または60%のコードが20%速くなる可能性があります。または、時間の80%がファイルの読み取りに費やされているときに、バッファを2倍のサイズにすると、2倍の速度になるいくつかの数学ルーチンを最適化します...

5
Mats Petersson

アセンブリコードをいじりたくない場合は、コンパイラフラグを微調整して、速度を最大限に最適化します。 gcc適切なARMターゲットを指定すると、ループの反復回数が明らかであれば、これを行う必要があります。

gccコード生成を確認するには、-Sフラグを追加してアセンブリ出力をリクエストします。

(gccのドキュメントを読んだり、フラグを微調整したりして)何度か試しても、必要なコードを生成できない場合は、アセンブリの出力を取得して、満足のいくように編集してください。


時期尚早の最適化 に注意してください。適切な開発順序は、コードを機能させることであり、それが最適化が必要かどうかを確認します。コードが安定している場合にのみ、そうすることが理にかなっています。

4
wallyk

この質問を提出してから長い時間が経過しましたが、関心を集めることに気づき、結局何をしたのかを話しました。

私の主な目標は、プロジェクトのボトルネックであるforループを最適化することでした。だから、私はアセンブリについて何も知らないので、NEON組み込み関数を試してみることにしました。 最終的に(このループのみで)パフォーマンスが40〜50%向上し、プロジェクト全体のパフォーマンスが全体的に大幅に向上しました。

コードは、生の距離データの束をミリメートル単位で平面までの距離に変換するいくつかの計算を行います。ここでは定義されていない定数(_constant05、_fXtoZなど)を使用していますが、これらは他の場所で定義されている定数値にすぎません。ご覧のとおり、私は一度に4つの要素の計算を行っています。実際の並列化について説明します:)

unsigned short* frameData = frame.ptr<unsigned short>(_depthLimits.y, _depthLimits.x);

unsigned short step = _runWidth - _actWidth; //because a ROI being processed, not the whole image

cv::Mat distToPlaneMat = cv::Mat::zeros(_runHeight, _runWidth, CV_32F);

float* fltPtr = distToPlaneMat.ptr<float>(_depthLimits.y, _depthLimits.x); //A pointer to the start of the data

for(unsigned short y = _depthLimits.y; y < _depthLimits.y + _depthLimits.height; y++)
{
    for (unsigned short x = _depthLimits.x; x < _depthLimits.x + _depthLimits.width - 1; x +=4)
    {
        float32x4_t projX = {(float)x, (float)(x + 1), (float)(x + 2), (float)(x + 3)};
        float32x4_t projY = {(float)y, (float)y, (float)y, (float)y};

        framePixels = vld1_u16(frameData);

        float32x4_t floatFramePixels = {(float)framePixels[0], (float)framePixels[1], (float)framePixels[2], (float)framePixels[3]};

        float32x4_t fNormalizedY = vmlsq_f32(_constant05, projY, _yResInv);

        float32x4_t auxfNormalizedX = vmulq_f32(projX, _xResInv);
        float32x4_t fNormalizedX = vsubq_f32(auxfNormalizedX, _constant05);

        float32x4_t realWorldX = vmulq_f32(fNormalizedX, floatFramePixels);

        realWorldX = vmulq_f32(realWorldX, _fXtoZ);

        float32x4_t realWorldY = vmulq_f32(fNormalizedY, floatFramePixels);
        realWorldY = vmulq_f32(realWorldY, _fYtoZ);

        float32x4_t realWorldZ = floatFramePixels;

        realWorldX = vsubq_f32(realWorldX, _tlVecX);
        realWorldY = vsubq_f32(realWorldY, _tlVecY);
        realWorldZ = vsubq_f32(realWorldZ, _tlVecZ);

        float32x4_t distAuxX, distAuxY, distAuxZ;

        distAuxX = vmulq_f32(realWorldX, _xPlane);
        distAuxY = vmulq_f32(realWorldY, _yPlane);
        distAuxZ = vmulq_f32(realWorldZ, _zPlane);

        float32x4_t distToPlane = vaddq_f32(distAuxX, distAuxY);
        distToPlane = vaddq_f32(distToPlane, distAuxZ);

        *fltPtr = (float) distToPlane[0];
        *(fltPtr + 1) = (float) distToPlane[1];
        *(fltPtr + 2) = (float) distToPlane[2];
        *(fltPtr + 3) = (float) distToPlane[3];

        frameData += 4;
        fltPtr += 4;
    }
    frameData += step;
    fltPtr += step;
}
3
Pedro Batista

QEMUで最小限のアセンブリの例を試して、手順を理解してください

次の設定にはまだ多くの例はありませんが、きちんとした遊び場として機能します。

これらの例はQEMUユーザーモードで実行され、追加のハードウェアが不要になり、GDBは正常に動作しています。

アサートはC標準ライブラリを介して行われます。

あなたはそれらを学びながら、新しい指示でそのセットアップを簡単に拡張できるはずです。

特にARM組み込み関数は、次の場所で尋ねられました: ARMネオン組み込み関数の適切なリファレンスはありますか?

0
Ciro Santilli