5

Linux上で実行されているC++プログラムがあり、メインスレッドとは関係なく計算コストの高い作業を行うために新しいスレッドが作成されています(計算作業は結果をファイルに書き込むことで完了し、最終的には非常に大きくなります)。ただし、パフォーマンスが比較的低下しています。

プログラムを(他のスレッドを導入せずに)簡単に実装すると、約2時間でタスクが完了します。マルチスレッドプログラムでは、同じタスクを実行するのに約12時間かかります(これは、生成された1つのスレッドのみでテストされました)。

スレッドを単一のCPUに設定するためのpthread_setaffinity_np (使用しているサーバーで使用可能な24個のうち)や、スケジューリングポリシーを設定するためのpthread_setschedparam(SCHED_BATCHのみを試した)など、いくつか試しました。 )。しかし、これらの影響はこれまで無視できる程度でした。

この種の問題の一般的な原因はありますか?

編集:私が使用しているいくつかのサンプルコードを追加しました。これは、うまくいけば最も関連性の高い部分です。関数process_job()は実際に計算作業を行うものですが、ここに含めるには多すぎます。基本的に、2つのデータファイルを読み込み、これらを使用してインメモリグラフデータベースでクエリを実行します。このデータベースでは、結果が2つの大きなファイルに数時間にわたって書き込まれます。

編集パート2:明確にするために、問題は、私が持っているアルゴリズムのパフォーマンスを向上させるためにスレッドを使用したいということではありません。むしろ、アルゴリズムの多くのインスタンスを同時に実行したいと思います。したがって、マルチスレッドをまったく使用しなかった場合と同じように、スレッドに入れたときにアルゴリズムが実行されると思います。

編集パート3:すべての提案をありがとう。一部の人が示唆しているように、私は現在いくつかのユニットテストを行っています(どの部分が遅くなっているのかを確認しています)。プログラムの読み込みと実行に時間がかかるため、テストの結果を確認するのに時間がかかります。そのため、応答が遅れたことをお詫び申し上げます。私が明らかにしたかった主なポイントは、スレッド化によってプログラムの実行が遅くなる可能性がある理由の可能性があると思います。私がコメントから集めたものから、それは単にそうであるべきではありません。妥当な解決策が見つかったら投稿します。ありがとうございます。

(最終)編集パート4:結局のところ、問題はスレッド化に関連していないことがわかりました。この時点で説明するのは面倒ですが(コンパイラー最適化レベルの使用を含む)、ここに投稿されたアイデアは非常に役に立ち、高く評価されました。

struct sched_param sched_param = {
    sched_get_priority_min(SCHED_BATCH)
};

int set_thread_to_core(const long tid, const int &core_id) {
   cpu_set_t mask;
   CPU_ZERO(&mask);
   CPU_SET(core_id, &mask);
   return pthread_setaffinity_np(tid, sizeof(mask), &mask);
}

void *worker_thread(void *arg) {
   job_data *temp = (job_data *)arg;  // get the information for the task passed in
   ...
   long tid = pthread_self();
   int set_thread = set_thread_to_core(tid, slot_id);  // assume slot_id is 1 (it is in the test case I run)
   sched_get_priority_min(SCHED_BATCH);
   pthread_setschedparam(tid, SCHED_BATCH, &sched_param);
   int success = process_job(...);  // this is where all the work actually happens
   pthread_exit(NULL);
}

int main(int argc, char* argv[]) {
   ...
   pthread_t temp;
   pthread_create(&temp, NULL, worker_thread, (void *) &jobs[i]);  // jobs is a vector of a class type containing information for the task
   ...
   return 0;
}
4

9 に答える 9

33

十分なCPUコアがあり、実行する作業が多い場合、マルチスレッドモードでの実行にシングルスレッドモードよりも長くかかることはありません。実際のCPU時間はわずかに長くなる可能性がありますが、「実時間」は短い。あなたのコードには、一方のスレッドがもう一方のスレッドをブロックしているという、ある種のボトルネックがあると確信しています。

これは、これらの1つ以上が原因です。最初にそれらをリストし、次に以下で詳細に説明します。

  1. スレッドの一部のロックにより、2番目のスレッドの実行がブロックされています。
  2. スレッド間でのデータの共有(真または「偽」の共有)
  3. キャッシュスラッシング。
  4. スラッシングやブロッキングを引き起こす外部リソースをめぐる競争。
  5. 一般的に不適切に設計されたコード...

スレッドの一部のロックにより、2番目のスレッドの実行がブロックされています。

ロックを取得するスレッドがあり、別のスレッドがこのスレッドによってロックされているリソースを使用したい場合は、待機する必要があります。これは明らかに、スレッドが何も役に立たないことを意味します。短時間だけロックをかけることで、ロックを最小限に抑える必要があります。次のようなコードを使用して、ロックがコードを保持しているかどうかを識別します。

while (!tryLock(some_some_lock))
{
    tried_locking_failed[lock_id][thread_id]++;
}
total_locks[some_lock]++;

ロックの統計を印刷すると、ロックが競合している場所を特定するのに役立ちます。または、「デバッガーでブレークを押して現在地を確認する」という古いトリックを試すことができます。スレッドが常にロックを待機している場合は、それが原因です。進行を防ぐ...

スレッド間でのデータの共有(真または「偽」の共有)

2つのスレッドが同じ変数を使用する[そしてその値を頻繁に更新する]場合、2つのスレッドは「これを更新しました」というメッセージを交換する必要があり、CPUは続行する前に他のCPUからデータをフェッチする必要があります変数を使用します。「データ」は「キャッシュラインごと」のレベルで共有され、キャッシュラインは通常32バイトであるため、次のようになります。

int var[NUM_THREADS]; 
...
var[thread_id]++; 

「偽共有」と呼ばれるものとして分類されます。更新される実際のデータはCPUごとに一意ですが、データは同じ32バイト領域内にあるため、コアは同じメモリ領域を更新します。

キャッシュスラッシング。

2つのスレッドが大量のメモリの読み取りと書き込みを行う場合、CPUのキャッシュは、他のスレッドのデータで埋めるために、常に適切なデータを破棄している可能性があります。CPUがキャッシュのどの部分を使用するかについて、2つのスレッドが「ロックステップ」で実行されないようにするために利用できるいくつかの手法があります。データが2^n(2の累乗)でかなり大きい(キャッシュサイズの倍数)場合は、スレッドごとに「オフセットを追加」することをお勧めします(たとえば、1KBまたは2KB)。そうすれば、2番目のスレッドがデータ領域に同じ距離を読み取るときに、最初のスレッドが現在使用しているのとまったく同じキャッシュ領域が上書きされることはありません。

スラッシングやブロッキングを引き起こす外部リソースをめぐる競争。

2つのスレッドがハードディスク、ネットワークカード、またはその他の共有リソースとの間で読み取りまたは書き込みを行っている場合、1つのスレッドが別のスレッドをブロックし、パフォーマンスが低下する可能性があります。また、他のスレッドで作業を開始する前に、コードが異なるスレッドを検出し、データが正しい順序などで書き込まれるように追加のフラッシュを実行することも可能です。

複数のスレッドが同じリソースを使用している場合にブロックするリソース(ユーザーモードライブラリまたはカーネルモードドライバー)を処理するコードの内部にロックがある可能性もあります。

一般的に悪いデザイン

これは、「間違っている可能性のある他の多くのこと」の「キャッチオール」です。一方のスレッドでの一方の計算の結果がもう一方のスレッドを進めるために必要な場合、明らかに、そのスレッドで多くの作業を行うことはできません。

ワークユニットが小さすぎるため、スレッドの開始と停止にすべての時間が費やされ、十分な作業が行われていません。たとえば、各スレッドに「これが素数であるかどうかを計算する」ために小さな数を一度に1つずつ実行するとします。スレッドに数を与えるには、「これは」の計算よりもはるかに長い時間がかかる可能性があります。実際には素数」-解決策は、各スレッドに一連の数値(おそらく、10、20、32、64など)を与えてから、ロット全体の結果を一度に報告することです。

他にもたくさんの「悪いデザイン」があります。コードを理解しないと、確実に言うのは非常に困難です。

あなたの問題が私がここで言及したもののどれでもない可能性は完全にありますが、おそらくそれはこれらの1つです。うまくいけば、この回答が原因の特定に役立つことを願っています。

于 2013-03-02T18:49:42.940 に答える
6

CPUキャッシュと、1つのスレッドから複数のスレッドへのアルゴリズムのナイーブポートがパフォーマンスの大幅な低下とスケーラビリティの低下をもたらすことが多い理由を理解するために注意してください。並列処理用に特別に設計されたアルゴリズムは、過度にアクティブなインターロック操作、偽共有、およびその他のキャッシュ汚染の原因を処理します。

于 2013-03-02T18:29:10.513 に答える
4

ここにあなたが調べたいと思うかもしれないいくつかの事柄があります。

1°)ワーカースレッドとメインスレッドの間にクリティカルセクション(ロック、セマフォなど)を入力しますか?(これは、クエリがグラフを変更する場合に当てはまります)。もしそうなら、それはマルチスレッドのオーバーヘッドの原因の1つである可能性があります。ロックをめぐって競合するスレッドは、通常、パフォーマンスを低下させます。

2°)24コアのマシンを使用していますが、これはNUMA(Non-Uniform Memory Access)だと思います。テスト中にスレッドアフィニティを設定するため、ハードウェアのメモリトポロジに細心の注意を払う必要があります。/ sys / devices / system / cpu / cpuX /内のファイルを確認すると、そのために役立ちます(cpu0とcpu1は必ずしも近接しているとは限らないため、必ずしもメモリを共有しているとは限らないことに注意してください)。メモリを多用するスレッドは、ローカルメモリを使用する必要があります(実行しているコアと同じNUMAノードに割り当てられます)。

3°)ディスクI/Oを多用しています。それはどのようなI/Oですか?すべてのスレッドが同期I/Oのたびに実行する場合は、非同期システムコールを検討して、OSがディスクへのこれらの要求のスケジューリングを引き続き実行できるようにすることができます。

4°)いくつかのキャッシュの問題は、他の回答ですでに言及されています。経験から、偽共有はあなたが観察しているのと同じくらいパフォーマンスを損なう可能性があります。私の最後の推奨事項(これは私の最初のはずです)は、LinuxPerfやOProfileなどのプロファイラーツールを使用することです。あなたが経験しているそのようなパフォーマンスの低下により、原因は確かに非常に明確に現れます。

于 2013-03-06T02:17:52.107 に答える
2

他の回答はすべて、症状を引き起こす可能性のある一般的なガイドラインに対応しています。私は自分自身の、できれば過度に冗長ではないバージョンを提供します。次に、説明したすべてのことを念頭に置いて、問題の根底に到達する方法について少し説明します。

一般に、複数のスレッドのパフォーマンスが向上すると予想される理由はいくつかあります。

  • 作業の一部は一部のリソース(ディスク、メモリ、キャッシュなど)に依存しますが、他の作業はこれらのリソースまたは前述のワークロードとは独立して続行できます。
  • ワークロードを並行して処理できる複数のCPUコアがあります。

上に列挙した主な理由は、複数のスレッドのパフォーマンスが低下すると予想される主な理由は、すべてリソースの競合に基づいています。

  • ディスクの競合:すでに詳細に説明されており、特にバッチ処理ではなく一度に小さなバッファを書き込んでいる場合は、問題が発生する可能性があります。
  • スレッドが同じコアにスケジュールされている場合のCPU時間の競合:アフィニティを設定している場合は、おそらく問題ではありません。ただし、それでも再確認する必要があります
  • キャッシュスラッシング:同様に、アフィニティがある場合はおそらく問題にはなりませんが、問題がある場合は非常にコストがかかる可能性があります。
  • 共有メモリ:ここでも詳細に説明されており、問題ではないようですが、コードを監査してチェックアウトしても問題はありません。
  • NUMA:再び話しました。ワーカースレッドが別のコアに固定されている場合は、アクセスする必要のある作業がメインコアに対してローカルであるかどうかを確認する必要があります。

これまでのところ、それほど新しいものではありません。上記のいずれか、またはまったくない場合があります。問題は、あなたの場合、延長戦がどこから来ているのかをどのように検出できるかということです。いくつかの戦略があります:

  • コードを監査し、明らかな領域を探します。そもそもプログラムを書いたとしても、一般的には実りがないので、これを行うのにあまり時間をかけないでください。
  • シングルスレッドコードとマルチスレッドコードをリファクタリングして1つのprocess()関数を分離し、主要なチェックポイントでプロファイリングして違いを説明します。次に、それを絞り込みます。
  • リソースアクセスをバッチにリファクタリングしてから、コントロールと実験の両方で各バッチのプロファイルを作成して、違いを説明します。これにより、どの領域(ディスクアクセス、メモリアクセス、タイトループでの時間を費やす)に集中する必要があるかがわかるだけでなく、このリファクタリングを実行すると、全体的な実行時間が改善される可能性があります。例:
    • 最初にグラフ構造をスレッドローカルメモリにコピーします(シングルスレッドの場合はストレートアップコピーを実行します)
    • 次に、クエリを実行します
    • 次に、ディスクへの非同期書き込みを設定します
  • 同じ症状で再現性が最小限のワークロードを見つけてください。これは、アルゴリズムを変更して、すでに実行していることのサブセットを実行することを意味します。
  • 違いを引き起こした可能性のある他のノイズがシステムにないことを確認してください(他のユーザーがワークコアで同様のシステムを実行している場合)。

あなたのケースに対する私自身の直感:

  • グラフ構造は、ワーカーコアにとってNUMAに適していません。
  • カーネルは実際にアフィニティコアからワーカースレッドをスケジュールできます。これは、ピン留めしているコアのisolcpuがオンになっていない場合に発生する可能性があります。
于 2013-03-07T23:19:33.283 に答える
2

詳細な分析を行うのに十分な情報を共有していないため、プログラムの何が問題になっているのかをお伝えすることはできません。

これが私の問題である場合、私が最初に試みることは、アプリケーションで2つのプロファイラーセッションを実行することです。1つはシングルスレッドバージョンで、もう1つはデュアルスレッド構成で実行します。プロファイラーレポートは、延長戦がどこに向かっているのかをかなりよく理解できるはずです。問題によっては、数秒または数分間プロファイリングした後に時差が明らかになる場合があるため、アプリケーションの実行全体をプロファイリングする必要がない場合があることに注意してください。

Linuxのプロファイラーの選択肢に関しては、 oprofileまたは2番目の選択肢のgprofを検討することをお勧めします。

プロファイラー出力の解釈についてサポートが必要な場合は、質問に自由に追加してください。

于 2013-03-09T05:09:57.457 に答える
1

スレッドが計画どおりに機能しない理由を突き止めるのは、後部での正しい苦痛になる可能性があります。分析的に行うことも、ツールを使用して何が起こっているかを示すこともできます。LinuxのSolarisのdtraceのクローンであるftraceからは、非常に優れたマイレージが得られました(これは、VxWorks、GreenhillのIntegrity OS、Mercury Computer Systems Incが長い間行ってきたことに基づいています)。

特に、このページは非常に便利だと思いました:http ://www.omappedia.com/wiki/Installing_and_Using_Ftrace 、特にこれこのセクション。OMAP指向のWebサイトであることを心配する必要はありません。私はそれをX86Linuxで問題なく使用しました(ただし、それを含めるにはカーネルを構築する必要があるかもしれません)。また、GTKWaveビューアは、主にVHDL開発からのログトレースを表示することを目的としているため、「奇妙」に見えることを忘れないでください。誰かがそれがsched_switchデータにも使用できるビューアであることに気づき、それを作成する手間を省いただけです。

sched_switchトレーサーを使用すると、スレッドがいつ実行されているか(必ずしも理由ではない)を確認でき、それで手がかりが得られる可能性があります。「なぜ」は、他のトレーサーのいくつかを注意深く調べることで明らかになります。

于 2013-03-09T07:42:25.970 に答える
0

1つのスレッドを使用することで速度が低下する場合は、スレッドセーフなライブラリ関数の使用またはスレッドのセットアップによるオーバーヘッドが原因である可能性があります。ジョブごとにスレッドを作成すると、かなりのオーバーヘッドが発生しますが、おそらく参照するほどではありません。言い換えれば、それはおそらくスレッドセーフなライブラリ関数からのオーバーヘッドです。

最善の方法は、コードのプロファイルを作成して、時間が費やされている場所を見つけることです。ライブラリ呼び出しにある場合は、代替ライブラリを見つけるか、自分で実装してみてください。ボトルネックがスレッドの作成/破棄である場合は、たとえばOpenMPタスクまたはC++11のstd::asyncを使用して、スレッドを再利用してみてください。

一部のライブラリは、スレッドセーフなオーバーヘッドで本当に厄介です。たとえば、多くのrand()実装は、スレッドローカルprgnを使用するのではなく、グローバルロックを使用します。このようなロックのオーバーヘッドは、数値を生成するよりもはるかに大きく、プロファイラーなしでは追跡が困難です。

速度低下は、変数を揮発性と宣言するなど、通常は必要ないはずの小さな変更に起因する場合もあります。

于 2013-03-10T17:03:52.070 に答える
0

シングルコアプロセッサが1つ搭載されたマシンで実行していると思われます。この問題は、その種のシステムでは並列化できません。コードは常にプロセッサを使用しています。プロセッサには、提供するサイクル数が固定されています。追加のスレッドが問題に高価なコンテキスト切り替えを追加するため、実際には実行速度が遅くなります。

シングルプロセッサマシンでうまく並列化される唯一の種類の問題は、実行の1つのパスを実行し、別のパスをI / Oの待機中にブロックする問題と、1つのスレッドが取得できる状況(応答性の高いGUIの維持など)です。コードをできるだけ早く実行するよりも、プロセッサ時間の方が重要です。

于 2013-03-11T17:06:54.183 に答える
0

アルゴリズムの多くの独立したインスタンスのみを実行したい場合は、クラスターに複数のジョブ(異なるパラメーターを使用し、単一のスクリプトで処理できます)を送信できますか?これにより、マルチスレッドプログラムのプロファイルを作成してデバッグする必要がなくなります。私はマルチスレッドプログラミングの経験があまりありませんが、MPIまたはOpenMPを使用する場合は、簿記のために書く必要のあるコードも少なくて済みます。たとえば、いくつかの一般的な初期化ルーチンが必要であり、その後プロセスを独立して実行できる場合は、1つのスレッドで初期化してブロードキャストを実行するだけで、それを実行できます。ロックなどを維持する必要はありません。

于 2013-03-11T19:35:03.880 に答える