
私は Buildroot を使用して、Raspberry PI3 用の独自の組み込み Linux OS を構築しています。この OS は複数のアプリケーションを処理するために使用され、そのうちの 1 つは OpenCV (v3.3.0) に基づいてオブジェクト検出を実行します。
私は Raspbian Jessy + Python から始めましたが、簡単な例を実行するのに非常に時間がかかることが判明したため、Python ではなく、最適化された機能 + C++ 開発を備えた独自の RTOS を設計することにしました。
これらの最適化により、RPI の 4 つのコアと 1 GB の RAM でこのようなアプリケーションを処理できると考えました。問題は、これらの最適化を行っても、最も単純なコンピューター ビジョン プログラムでも時間がかかることです。
PC と Raspberry PI3 の比較
これは、プログラムの各部分の実行時間の大きさを把握するために作成した簡単なプログラムです。
#include <stdio.h>
#include "opencv2/core.hpp"
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include <time.h> /* clock_t, clock, CLOCKS_PER_SEC */
using namespace cv;
using namespace std;
int main()
{
setUseOptimized(true);
clock_t t_access, t_proc, t_save, t_total;
// Access time.
t_access = clock();
Mat img0 = imread("img0.jpg", IMREAD_COLOR);// takes ~90ms
t_access = clock() - t_access;
// Processing time
t_proc = clock();
cvtColor(img0, img0, CV_BGR2GRAY);
blur(img0, img0, Size(9,9));// takes ~18ms
t_proc = clock() - t_proc;
// Saving time
t_save = clock();
imwrite("img1.jpg", img0);
t_save = clock() - t_save;
t_total = t_access + t_proc + t_save;
//printf("CLOCKS_PER_SEC = %d\n\n", CLOCKS_PER_SEC);
printf("(TEST 0) Total execution time\t %d cycles \t= %f ms!\n", t_total,((float)t_total)*1000./CLOCKS_PER_SEC);
printf("---->> Accessing in\t %d cycles \t= %f ms.\n", t_access,((float)t_access)*1000./CLOCKS_PER_SEC);
printf("---->> Processing in\t %d cycles \t= %f ms.\n", t_proc,((float)t_proc)*1000./CLOCKS_PER_SEC);
printf("---->> Saving in\t %d cycles \t= %f ms.\n", t_save,((float)t_save)*1000./CLOCKS_PER_SEC);
return 0;
}
Raspberry PI での実行結果 (Buildroot から生成された OS)
ご覧の通り、大きな違いがあります。必要なのは、この例が処理ステップは「ほぼ」リアルタイムで発生し、最大15msの時間で。
私の質問は次の通りです:
- 集中的な計算アプリケーションを処理できるように OS を最適化し、各部分の優先順位を制御するにはどうすればよいでしょうか?
- RPI3 の 4 つのコアを最大限に活用して要件を満たすにはどうすればよいですか?
- OpenCV の代わりに何か他の可能性はありますか?
- C++ の代わりに C を使うべきでしょうか?
- ハードウェアの改善をお勧めしますか?
答え1
順番に:
集中的な計算アプリケーションを処理できるように OS を最適化し、各部分の優先順位を制御するにはどうすればよいでしょうか?
一般的な最適化については、実際に必要なものだけがバックグラウンドで実行されていることを確認するなどの通常の作業以外に、OS 側でできることはあまりありません。オリジナルの Pi では、これらの関数のアセンブリ最適化バージョンを提供する「cofi」というライブラリを使用するmemmove()
ことで、同様の関数を高速化できましたLD_PRELOAD
が、Pi 3 で役立つかどうかはわかりません。
優先順位付けについては、実際にはマニュアル ページを参照する必要がありますが、通常は並列化を行わない限り実行できません (この場合、明らかな解決策は、各ステップをプロセスとして実行し、IPC (パフォーマンス上の理由からおそらく共有メモリ) を使用してそれらの間でデータを移動することです)。
テスト プログラムから引用した結果について、特に、処理と保存のステップはどちらも Pi では約 10 倍遅いのに対し、アクセスのステップは約 5 倍遅いことに留意してください。これらの数値は、Pi 3 と 1 年未満の一般的な PC を比較した場合に予想される大まかな見積もりと一致します。Pi の CPU は、PC テストを実行した CPU よりも大幅に遅いことはほぼ確実です (並列化をまったく行わなかった場合、その差はさらに広がります。最新の x86 CPU のほとんどは、1 つのコアをフル ロードで実行する方が、すべてのコアをフル ロードで実行するよりもはるかに高速です)。これが影響します。 ARM ISA も x86 ISA とは大きく異なります (ARM は x86 に比べてサイクルあたりの処理が少ない傾向がありますが、通常は RAM に頻繁にアクセスする必要がなく、分岐予測ミスが x86 ほどコストがかかることもありません)。そのため、GCC が PC 上で物事を配置する方法に最適化されたコードは、Pi ではそれほど最適にはなりません。
あなたが使用しているカメラが何であるかはわかりませんが、処理する画像の解像度を下げることで時間を短縮できると思います。また、圧縮形式の使用を避ければ、おそらく取得時間も短縮できます (非可逆圧縮を使用しないということは、解像度がそれほど重要ではないことを意味します)。
RPI3 の 4 つのコアを最大限に活用して要件を満たすにはどうすればよいですか?
独自のコードで並列化を行います。カーネルで SMP が有効になっていることを確認し (RPi Foundation の公式構成を使用している場合は有効になっているはずです)、並列で実行してみてください。OpenCV 自体がどの程度並列化を行っているかはわかりませんが、OpenMP も検討するとよいかもしれません (相互依存していないループ内の反復を並列化する比較的簡単な方法を提供します)。
OpenCV の代わりに何か他の可能性はありますか?
あるかもしれませんが、誰もが OpenCV を標準化しているので、それを使用することをお勧めします (誰もが使用しているため、実装に関する技術的なサポートを受けるのが簡単になります)。
C++ の代わりに C を使うべきでしょうか?
それは、どのように使用しているかによります。C++ では C よりも遅いコードを書く方がはるかに簡単ですが、どちらの言語でも速いコードを書くのは同じくらい難しいです。多くの最適化手法は、どちらの言語でもかなり似ています (たとえば、起動時にすべてを事前割り当てして、malloc()
重要なセクションで を呼び出さないようにしたり、 の呼び出しを避けたりしますstat()
)。ただし、C++ の場合は、は避けてください。あらゆる場所でstd::string
を呼び出すためmalloc()
、結果的に非常に遅くなります ( から C スタイルの文字列への変換によって、パフォーマンスが 40% 以上向上するケースを見たことがありますstd::string
)。
ハードウェアの改善をお勧めしますか?
ハードウェアのコストを抑え、スペースに制約がある(したがって Raspberry Pi を選択)という前提では、思いつくものはほとんどありません。Pi(すべてのイテレーション)は、その価格帯でコンピューター ビジョン作業に非常に適した SoC を使用しています。もう少し大きくて、やや高価なものを使用しても構わないのであれば、NVIDIA Jetson ボードをお勧めします(Quadro と同等の GPU と 192 個の CUDA コアが統合された Tegra SoC を使用しているため、処理ワークロードをはるかに高速に実行できる可能性があります)。ただし、Buildroot をそこで動作させるには、Pi よりもかなり手間がかかります。
コメントに応じて編集:
プロセス レベルでの並列化はマルチスレッド化と同じものではなく、大きく異なります (最大の違いはリソースの共有方法にあり、デフォルトではスレッドはすべてを共有し、プロセスは何も共有しません)。一般に、多くの処理が関係する場合は、スレッドの安全性を気にせずに効率的なコードを書く方が簡単なので、プロセス ベースの並列化を使用する方が (通常は) 適しています。
オプションに関して言えば、あなたが挙げた 2 つはシステム パフォーマンスに大きな影響を与える可能性がありますが、どちらも最終的にはスループットとレイテンシの間でトレードオフになります。プリエンプション モデルは、カーネル モードで実行されているもの (システム コールなど) を再スケジュールする方法を制御します。オプションは次の 3 つです。
- プリエンプションなし: これは、カーネル モードで実行されているものはすべて中断されないことを意味します。これは、SVR4 および 4.4BSD の動作、および他のほとんどの古い UNIX システムの動作と一致します。スループットには非常に優れていますが、レイテンシには非常に悪いため、通常は CPU を多数搭載した大規模なサーバーでのみ使用されます (CPU の数が多いほど、プリエンプト可能なものが実行されている可能性が高くなります)。
- 自発的プリエンプション: これにより、カーネル内の各関数が再スケジュールできる場所を定義できます。これは、スループットとレイテンシのバランスが適切に保たれるため、デスクトップをターゲットとしたほとんどの Linux ディストリビューションで使用される設定です。
- 完全なプリエンプション: これは、カーネル内の (ほぼ) すべてのコードが (ほぼ) いつでも中断される可能性があることを意味します。これは、リアルタイムのマルチメディア作業に使用されるシステムなど、入力および外部イベントに関して非常に低いレイテンシを必要とするシステムに役立ちます。スループットには絶対に悪いですが、レイテンシに勝るものはありません。
対照的に、タイマー周波数ははるかに簡単に説明できます。タイマー周波数は、実行を待機している他の何かがある場合に、何かが中断されることなく実行できる最長時間を制御します。値が高いほど時間が短くなり (レイテンシが低くなり、スループットも低くなります)、値が低いほど時間が長くなります (レイテンシが高くなり、スループットが高くなります)。一般的な開始方法としては、プリエンプション モデルを任意に設定し、タイマー周波数を 300 Hz に設定してから、最初にタイマー周波数を変更して実験することをお勧めします (通常、タイマー周波数の変更の方が目に見える影響があります)。
Movidius NCS については、その価値があるかどうかは、USB 接続によって帯域幅が制限されるため、処理する必要があるデータの量によって異なります (Pi には USB 2.0 コントローラーが 1 つしかないため、Movidius が設計されている帯域幅の 10 分の 1 未満に制限されるだけでなく、少なくとも Ethernet アダプターとバスを共有する必要があり、レイテンシとスループットの両方に悪影響を及ぼします)。 1920x1080 の 32 ビット カラーの単一フレームを低レートで処理するだけであれば、実行可能かもしれませんが、同じビデオをフル フレーム レートでストリーミング処理する必要がある場合は、レイテンシの問題が発生する可能性があります。 使用することを選択した場合は、必ず電源付きハブを入手してください (そうしないと、Pi が提供できる以上の電力を消費しようとする問題が発生する可能性があります)。