この記事は、インテル® ソフトウェア・ネットワークに掲載されている「Use Synchronization Routines Provided by the Threading API Rather than Hand-Coded Synchronization」 (https://software.intel.com/content/www/us/en/develop/articles/use-synchronization-routines-provided-by-the-threading-api-rather-than-hand-coded-synchronization.html) の日本語参考訳です。
編集注記:
本記事は、2011 年 5 月 4 日に公開されたものを、加筆・修正したものです。
開発者によっては、同期に伴うオーバーヘッドを削減したり、既存の構造にはない機能を追加するため、スレッド API の同期ルーチンの代わりに独自コードの同期ルーチンを使用することもあるでしょう。このようなコードの同期ルーチンは、マルチスレッド・アプリケーションのパフォーマンスやパフォーマンス・チューニング、デバッグに悪影響を与える場合があります。
この記事は、「マルチスレッド・アプリケーションの開発のためのガイド」の一部で、インテル® プラットフォーム向けにマルチスレッド・アプリケーションを効率的に開発するための手法について説明します。
はじめに
スレッド API の同期ルーチンに伴うオーバーヘッドを回避したり、スレッド API の同期ルーチンで必要な機能が提供されていない場合に独自コードによる同期を行うことはよくあります。しかしながら、それらのコードによる同期には、スレッド API ルーチンと比べて、いくつの重大なデメリットかあります。
その 1 つとして、独自コードの同期では、異なるハードウェア・アーキテクチャーやオペレーティング・システムで優れたパフォーマンスが得られにくいことが挙げられます。この問題を例示したスピンロックの C コードについて考えてみましょう。
#include void acquire_lock (int *lock) { while (_InterlockedCompareExchange (lock, TRUE, FALSE) == TRUE); } void release_lock (int *lock) { *lock = FALSE; }
_InterlockedCompareExchange コンパイラー組み込み関数は、実行中にほかのスレッドがメモリー・ロケーションの変更を行わないようにするメモリー・インターロック命令です。第 1 引数で指定されたメモリーアドレスの値と第 3 引数の値を比較して一致する場合は、第 2 引数の値を第 1 引数のメモリーアドレスにストアします。この関数は、指定されたメモリーアドレスにあるオリジナルの値を返します。
acquire_lock ルーチンは、メモリー・ロケーション lock の値がアンロック状態 (FALSE) になるまでスピンし、アンロック状態になったらロックを取得 (lock の値を TRUE に設定) します。release_lock ルーチンは、メモリー・ロケーション lock の値を FALSE に戻してロックを解放します。
このコードは単純で効率的に見えますが、いくつかの問題があります。
- 複数のスレッドが同じメモリー・ロケーションでスピンすると、ロック解放時にキャッシュ無効化とメモリー・トラフィックが頻発し、スレッド数が増えるほどスケーラビリティーが低下します。
- このコードで使用しているアトミックなメモリー更新手法は、特定のプロセッサー・アーキテクチャーでは利用できないため、移植性が制限されます。
- インテル® ハイパースレッディング・テクノロジーなどの特定のプロセッサー・アーキテクチャー機能では、スピンループの多用はパフォーマンスの低下につながります。
- オペレーティング・システムには、while ループが有益な計算を行っているように見えるため、オペレーティング・システムのスケジューリングの公平性に悪影響を与えます。
これらの問題にはすべて解決方法がありますが、通常それらを使用するとコードが複雑になる傾向があり、正当性の検証が難しくなります。また、移植性を維持しながらコードのチューニングを行うことは困難です。そのため、これらの問題はスレッド API に任せたほうが得策です。スレッド API では、移植性とスケーラビリティーを実現するため、多大な時間をかけて同期構造の検証とチューニングが行われています。
独自コードによる同期のもう 1 つの問題として、スレッド化ツールの精度を低下させることが挙げられます。例えば、インテル® Parallel Studio XE は、同期構造を特定して、マルチスレッド・アプリケーションのパフォーマンス (インテル® VTune™ Amplifier やインテル® VTune™ プロファイラーを使用) と正当性 (インテル® Inspector を使用) に関する情報を提供します。
通常、スレッド化ツールは、サポートしているスレッド API の同期構造の機能を特定し、判断するように設計されています。上記の例のように、標準的な同期 API を使用していない場合、スレッド化ツールが同期を特定して理解することが困難になります。
スレッド化ツールが独自コードの同期を特定するには、プログラマーがツール固有の宣言子、プラグマ、API 呼び出しなどの形式でヒントを指定できることがあります。ただし、このようなヒントがサポートされていても、同期 API を使用する場合と比べると、アプリケーション・プログラムの解析の精度は低下し、パフォーマンス問題の検出が困難であったり、正当性ツールでデータ競合や同期の欠落が誤って報告されることがあります。
アドバイス
独自コードによる同期はできるだけ避けて、スレッド API の標準ルーチンを使用してください。例えば、インテル® スレッディング・ビルディング・ブロック (インテル®TBB) の queuing_mutex や spin_mutex、OpenMP* の omp_set_lock/omp_unset_lock 関数や critical/end critical ディレクティブ、Pthreads* の pthread_mutex_lock/pthread_mutex_unlock 関数を使用します。スレッド API の同期ルーチンと同期構造を吟味した上で、アプリケーションに適したものを使用するようにしてください。
スレッド API に必要な機能を備えた同期がない場合、同期を減らしたり同期の方法を変更するなど、アルゴリズムを見直してみてください。経験豊富なプログラマーは、API による簡単な同期構造を基に独自の同期構造を作成することもできます。
パフォーマンス問題を解決するため独自コードによる同期を行う場合、前処理済みの宣言子を使用して、独自コードの同期を同等の機能を持つスレッド API に置換できるようにすることで、スレッド化ツールの精度を向上できます。
利用ガイド
API を利用した簡単な同期構造から、独自の同期構造を作成する場合は、パフォーマンスのスケーラビリティーを損ねないように、共有ロケーションでスピンループは使用しないでください。コードを移植する必要がある場合は、アトミックなメモリー・プリミティブの使用も避けたほうが良いでしょう。
スレッド化のパフォーマンス解析/正当性検証ツールは、API の簡単な同期構造を正しく特定できても、それを基に作成された独自の同期構文を正しく特定できないことがあります。この場合、ツールの精度は低下します。
関連情報
- John Mellor-Crummey, “Algorithms for Scalable Synchronization on Shared-Memory Multiprocessors.” ACM Transactions on Computer Systems, Vol. 9, No. 1, February 1991. Pages 21-65. (英語)
- Intel Pentium 4 and Intel Xeon Processor Optimization Reference Manual, Chapter 7: “Multiprocessor and Hyper-Threading Technology.” Order Number: 248966-007.
- インテル® Parallel Studio XE
- OpenMP* 仕様 (英語)
- インテル® TBB
- インテル® TBB オープンソース版 (英語)
- James Reinders, Intel Threading Building Blocks: Outfitting C++ for Multi-core Processor Parallelism. O’Reilly Media, Inc. Sebastopol, CA, 2007. (英語)