11

さまざまなサイズの画像を操作するプログラムに取り組んでいます。これらの操作の多くは、入力からピクセル データを読み取り、別の出力に書き込みます (ぼかしなど)。これはピクセル単位で行われます。

このような画像マッピングは、CPU に非常に負担がかかります。マルチスレッドを使用して高速化したいと考えています。どうすればいいですか?ピクセルの行ごとに 1 つのスレッドを作成することを考えていました。

いくつかの要件があります。

  • 実行可能サイズは最小化する必要があります。つまり、大規模なライブラリを使用することはできません。C/C++ 用の最も軽量で移植可能なスレッド化ライブラリは何ですか?
  • 実行可能サイズは最小化する必要があります。行ごとにスレッドを実行する forEachRow(fp* ) 関数、または fp が独自のスレッド内の単一のピクセルで動作する forEachPixel(fp* ) を使用することを考えていました。どれが最高ですか?
    • 通常の関数、ファンクター、functionoid、またはラムダ関数などを使用する必要がありますか?
    • 一部の操作では、前に処理されたピクセルからの情報を必要とする最適化が使用されます。これにより、forEachRow が有利になります。これを考慮しても forEachPixel を使用した方が良いでしょうか?
  • 読み取り専用および書き込み専用の配列をロックする必要がありますか?
    • 入力は読み取られるだけですが、多くの操作では配列内の複数のピクセルからの入力が必要です。
    • 出力はピクセルごとに 1 回だけ書き込まれます。
  • もちろん速度も重要ですが、実行可能ファイルのサイズを最適化することが優先されます。

ありがとう。

好奇心旺盛な方向けのこのトピックの詳細: C++ 並列化ライブラリ: OpenMP vs. スレッド ビルディング ブロック

4

16 に答える 16

14

軽々しく糸を通すな! 競合状態は、把握するのが大変な場合があります。特に、スレッドの経験があまりない場合は! (警告されました: ここにドラゴンがいます! 大きな毛むくじゃらの非決定論的で信頼性の高い再現が不可能なドラゴンです!)

デッドロックとは何か知っていますか?ライブロックはどうですか?

それは言った...


ckarmann と他の人がすでに提案しているように: ワーク キュー モデルを使用します。CPU コアごとに 1 つのスレッド。 作業を N 個のチャンクに分割します。多くの行のように、チャンクを適度に大きくします。各スレッドが解放されると、次の作業チャンクがキューから取り出されます。

最も単純なIDEALバージョンでは、N 個のコア、N 個のスレッド、および問題の N 個のサブパーツがあり、各スレッドは最初から何をするかを正確に認識しています。

ただし、スレッドの開始/停止のオーバーヘッドのため、実際には通常は発生しません。スレッドがすでに生成され、アクションを待機していることを本当に望んでいます。(たとえば、セマフォを介して。)

ワークキュー モデル自体は非常に強力です。これにより、通常は N スレッド/コア間で正常に並列化されないクイック ソートなどを並列化できます。


コアよりスレッドが多い?オーバーヘッドを浪費しているだけです。各スレッドにはオーバーヘッドがあります。#threads=#cores であっても、完全な Nx スピードアップ ファクターを達成することはできません。

行ごとに 1 つのスレッドは非常に非効率的です! 1 ピクセルあたり 1 スレッド?考えたくもない。(このピクセル単位のアプローチは、古い Cray のようにベクトル化されたプロセッサ ユニットで遊ぶ場合に、より理にかなっていますが、スレッドではそうではありません!)


図書館?あなたのプラットフォームは何ですか?Unix/Linux/g++ では、pthreads とセマフォをお勧めします。(Pthreads は、Microsoft 互換レイヤーを備えた Windows でも使用できます。しかし、うーん。私はそれをあまり信用していません! そこでは、Cygwin の方が適しているかもしれません。)

Unix/Linux では、man :

* pthread_create, pthread_detach.
* pthread_mutexattr_init, pthread_mutexattr_settype, pthread_mutex_init,
* pthread_mutexattr_destroy, pthread_mutex_destroy, pthread_mutex_lock,
* pthread_mutex_trylock, pthread_mutex_unlock, pthread_mutex_timedlock.
* sem_init, sem_destroy, sem_post, sem_wait, sem_trywait, sem_timedwait.

pthread の条件変数が好きな人もいます。しかし、私は常に POSIX 1003.1b セマフォを好みました。それらは、待機を開始するに別のスレッドにシグナルを送りたい状況を処理します。または、別のスレッドが複数回通知される場所。

ああ、あなた自身にお願いします。スレッド/ミューテックス/セマフォの pthread 呼び出しをいくつかの C++ クラスにラップします。これにより、問題が大幅に簡素化されます。


読み取り専用および書き込み専用の配列をロックする必要がありますか?

正確なハードウェアとソフトウェアに依存します。通常、読み取り専用配列はスレッド間で自由に共有できます。しかし、そうではない場合もあります。

書くことはほとんど同じです。通常、1 つのスレッドだけが特定の各メモリ スポットに書き込みを行っている限り、問題はありません。しかし、そうではない場合もあります!

これらの奇妙なフェンスポストの状況に陥る可能性があるため、書くことは読むことよりも面倒です。多くの場合、メモリはバイトではなくワードとして書き込まれます。あるスレッドが単語の一部を書き込み、別のスレッドが別の部分を書き込む場合、どのスレッドがいつ何を行うかの正確なタイミング (非決定論的など) によっては、非常に予測不可能な結果が生じる可能性があります。

私はそれを安全にプレイします。各スレッドに読み取り領域と書き込み領域の独自のコピーを与えます。完了したら、データをコピーして戻します。もちろん、すべてミューテックスの下にあります。

ギガバイト単位のデータについて話している場合を除き、メモリ ブライトは非常に高速です。その数マイクロ秒のパフォーマンス時間は、デバッグの悪夢に値するものではありません。

ミューテックスを使用してスレッド間で 1 つの共通データ領域を共有すると、衝突/待機中のミューテックスの非効率性が積み重なり、効率が低下します!


ほら、クリーンなデータ境界は、優れたマルチスレッド コードの本質です。境界が明確でない場合、問題が発生します。

同様に、境界上のすべてをミューテックス状態に保つことが不可欠です。そして、ミューテックスされた領域を短く保つために!

同時に複数のミューテックスをロックしないようにしてください。複数のミューテックスをロックする場合は、常に同じ順序でロックしてください!

可能であれば、ERROR-CHECKING または RECURSIVE ミューテックスを使用してください。FAST ミューテックスは、実際の (測定された) 速度の向上がほとんどなく、トラブルを求めているだけです。

デッドロック状態になった場合は、gdb で実行し、ctrl-c を押して各スレッドにアクセスし、バックトレースします。そうすれば、問題を非常に迅速に見つけることができます。(ライブロックはもっと難しいです!)


最後に 1 つの提案: シングルスレッドでビルドしてから、最適化を開始してください。シングルコア システムでは、スレッド化よりも foo[i++]=bar ==> *(foo++)=bar のようなものの方が速度が向上することがあります。


補遺:ミューテックスされた領域を上に短く保つこと について私が言ったことは何ですか? 2 つのスレッドを考えてみましょう: (Mutex クラスのグローバル共有ミューテックス オブジェクトがあるとします)。

/*ThreadA:*/ while(1){  mutex.lock();  printf("a\n");  usleep(100000); mutex.unlock(); }
/*ThreadB:*/ while(1){  mutex.lock();  printf("b\n");  usleep(100000); mutex.unlock(); }

何が起こるか?

私のバージョンの Linux では、1 つのスレッドが継続的に実行され、もう 1 つのスレッドは枯渇します。ごくまれに、mutex.unlock() と mutex.lock() の間でコンテキスト スワップが発生したときに場所が変更されます。


補遺: あなたの場合、これが問題になる可能性は低いです。しかし、他の問題では、特定の作業チャンクが完了するまでにかかる時間を事前に知ることができない場合があります。問題を (4 つの部分ではなく) 100 の部分に分割し、work-queue を使用して 4 つのコアに分割すると、このような不一致が平滑化されます。

ある作業チャンクが完了するまでに別の作業チャンクの 5 倍の時間がかかる場合、最終的にはすべてが均等になります。チャンクが多すぎると、新しい作業チャンクを取得するオーバーヘッドにより、顕著な遅延が生じます。これは、問題固有のバランスをとる行為です。

于 2008-11-29T04:04:26.113 に答える
10

コンパイラがOpenMPをサポートしている場合 (gcc と同様にVC++ 8.0 と 9.0がサポートしていることは知っています)、このようなことをはるかに簡単に行うことができます。

多くのスレッドを作成したいだけではありません。新しいスレッドを追加すると、コンテキストの切り替えが増えるにつれて速度が低下するという収穫逓減のポイントがあります。ある時点で、あまりにも多くのスレッドを使用すると、線形アルゴリズムを使用するよりも実際に並列バージョンが遅くなる可能性があります。最適なスレッド数は、使用可能な CPU/コアの数と、各スレッドが I/O などでブロックされる時間の割合の関数です。並列パフォーマンスの向上に関する議論については、Herb Sutter による この記事を参照してください。

OpenMP を使用すると、作成されるスレッドの数を利用可能な CPU の数に簡単に適応させることができます。これを (特にデータ処理の場合に) 使用するには、多くの場合#pragma omp、既存のコードにいくつかの s を挿入し、スレッドの作成と同期をコンパイラに処理させるだけです。

一般に、データが変更されない限り、読み取り専用データをロックする必要はありません。各ピクセル スロットが 1 回だけ書き込まれることが確実であり、結果から読み取りを開始する前にすべての書き込みが完了していることを保証できる場合は、それをロックする必要もありません。

OpenMP の場合、ファンクター/関数オブジェクトに関する限り、特別なことをする必要はありません。あなたにとって最も意味のある方法で書いてください。Intelの画像処理の例を次に示します(RGB をグレースケールに変換します)。

#pragma omp parallel for
for (i=0; i < numPixels; i++)
{
   pGrayScaleBitmap[i] = (unsigned BYTE)
       (pRGBBitmap[i].red * 0.299 +
        pRGBBitmap[i].green * 0.587 +
        pRGBBitmap[i].blue * 0.114);
}

これにより、CPU と同じ数のスレッドに自動的に分割され、配列のセクションが各スレッドに割り当てられます。

于 2008-11-28T19:50:31.947 に答える
6

私はお勧めboost::threadboost::gilます(一般的な画像ライブラリ)。非常に多くのテンプレートが含まれているため、コード サイズがまだ許容できるかどうかはわかりません。しかし、これはブーストの一部なので、おそらく一見の価値があります。

于 2008-11-28T19:31:08.193 に答える
2

少し左翼的な考えとして...

どのシステムでこれを実行していますか? PC で GPU を使用することを考えたことはありますか?

Nvidiaには、この種のことのためのCUDA APIがあります

于 2008-11-28T19:39:50.173 に答える
1

並列パターン ライブラリを使用して同時画像処理パイプラインを構成する方法について説明している、MSDNの画像処理ネットワークの作成に関するチュートリアルを確認してください。

また、非常に効率的なコードを生成するBoost.GILもお勧めします。簡単なマルチスレッドの例については、Victor Bogado による gil_threadedを確認してください。Dataflow.Signals と Boost.GIL を使用した画像処理ネットワークは、興味深いデータフロー モデルについても説明しています。

于 2012-09-28T15:02:10.327 に答える
1

お使いのコンパイラは OpenMP をサポートしていません。もう 1 つのオプションは、ライブラリ アプローチを使用することです。Intel の Threading Building Blocks と Microsoft Concurrency Runtime の両方が利用可能です (VS 2010)。

両方のライブラリでサポートされている Parallel Pattern Library と呼ばれるインターフェイスのセットもあり、これらにはテンプレート化された parallel_for ライブラリ呼び出しがあります。代わりに:

#pragma omp parallel for 
for (i=0; i < numPixels; i++) 
{ ...} 

あなたは書くでしょう:

parallel_for(0,numPixels,1,ToGrayScale());

ここで、ToGrayScale は関数へのファンクターまたはポインターです。(コンパイラがラムダ式をサポートしている場合は、ファンクターをラムダ式としてインライン化できない可能性が高いことに注意してください)。

parallel_for(0,numPixels,1,[&](int i)
{  
   pGrayScaleBitmap[i] = (unsigned BYTE)  
       (pRGBBitmap[i].red * 0.299 +  
        pRGBBitmap[i].green * 0.587 +  
        pRGBBitmap[i].blue * 0.114);  
});

-リック

于 2010-01-15T15:55:26.847 に答える
1

これを書いているプラ​​ットフォームを尋ねてもよろしいですか? 実行可能ファイルのサイズは、デスクトップ マシンを対象としていない問題であるためだと思います。プラットフォームに複数のコアまたはハイパースレッドがあるのはどの場合ですか? そうでない場合、アプリケーションにスレッドを追加すると、逆の効果があり、速度が低下する可能性があります...

于 2008-11-28T19:37:35.920 に答える
1

単純な画像変換を最適化するには、プログラムをマルチスレッド化するよりも、SIMD ベクトル演算を使用する方がはるかに優れています。

于 2008-11-29T14:06:46.773 に答える
1

行ごとに1つのスレッドが必要だとは思いません。多くの行が存在する可能性があり、スレッドを起動/破棄し、CPU を別のスレッドに切り替えるだけで、多くのメモリ/CPU リソースを消費します。さらに、C コアを備えた P プロセッサを使用している場合、おそらく C*P スレッドよりも多くの利益を得ることはできません。

定義された数のクライアント スレッド (N スレッドなど) を使用し、アプリケーションのメイン スレッドを使用して各スレッドに行を分散するか、単に「ジョブ キュー」から命令を取得することをお勧めします。スレッドが行の処理を終了すると、このキューをチェックインして別の行を実行することができます。

ライブラリに関しては、boost::thread を使用できます。これは移植性が高く、重すぎません。

于 2008-11-28T19:33:24.867 に答える
0

選択したスレッドモデル(ブースト、pthread、ネイティブスレッドなど)に関係なく、私は思います。行ごとのスレッドではなく、スレッドプールを検討する必要があると思います。スレッドプール内のスレッドは、OSに関する限りすでに作成されているため、「開始」するのに非常に安価です。それは、何かを与えるだけの問題です。

基本的に、プールには4つのスレッドがあります。次に、シリアル方式で、ピクセルごとに、スレッドプール内の次のスレッドにピクセルを処理するように指示します。このようにして、一度に4ピクセル以下を効果的に処理します。プールのサイズは、ユーザー設定またはシステムが報告するCPUの数に基づいて作成できます。

これは、IMHOがSIMDタスクにスレッドを追加する最も簡単な方法です。

于 2008-11-29T04:39:39.210 に答える
0

map/reduce フレームワークは、この状況で使用するのに理想的なものになると思います。Hadoop ストリーミングを使用して、既存の C++ アプリケーションを使用できます。

マップを実装してジョブを削減するだけです。

あなたが言ったように、行レベルの操作を map タスクとして使用し、行レベルの操作を reduce タスクの最終的な画像に組み合わせることができます。

これが役に立つことを願っています。

于 2010-01-15T16:00:12.973 に答える
0

#ifdefプラットフォームごとにを使用して、いくつかの標準スレッド関数を実装する独自の小さなライブラリを作成することはできますか? 実際にはそれほど多くはありません。これにより、使用できるどのライブラリよりも実行可能ファイルのサイズが削減されます。

更新:そして、作業の配布のために - 画像を分割して、各スレッドに分割してください。ピースが完成したら完成です。こうすることで、実行可能ファイルのサイズをさらに大きくするジョブ キューの実装を回避できます。

于 2008-11-28T19:50:23.117 に答える
0

ピクセル行ごとに 1 つのスレッドは正気ではなく、n-1 から 2n スレッド (n cpu の場合) が最適であり、それぞれが 1 つのジョブユニット (1 つの行または他の種類のパーティション) をフェッチするループを作成します。

UNIXライクでは、シンプルで軽量なpthreadsを使用してください。

于 2008-11-28T19:33:48.127 に答える
0

ボトルネックはCPUではなくメモリ帯域幅である可能性が非常に高いため、マルチスレッドはあまり役に立ちません。より多くのデータをキャッシュできるように、メモリ アクセスを最小限に抑え、限られたメモリ ブロックで作業するようにしてください。少し前に同様の問題があり、SSE 命令を使用するようにコードを最適化することにしました。速度の向上は、シングル スレッドあたり約 4 倍でした。

于 2010-01-15T10:54:30.270 に答える
-3

最適化のためにアセンブリを使用する別のオプションがあります。現在、動的コード生成のためのエキサイティングなプロジェクトの 1 つがsoftwire です(これは少し前にさかのぼります -元のプロジェクトのサイトはここにあります)。これは Nick Capens によって開発され、現在市販されている swiftshader に成長しまし。しかし、元のソフトワイヤーのスピンオフは、gna.org で引き続き入手できます。

これは、彼のソリューションへの導入として役立ちます。

個人的には、問題に複数のスレッドを利用してもパフォーマンスが大幅に向上するとは思えません。

于 2008-11-28T21:50:56.243 に答える