マルチスレッド・アプリケーションの開発

マルチスレッド・アプリケ ーションの開発
2003 年 3 月
© 2003— 2004, Intel Corporation.
使用条件
本書は、市場性、他者の権利を侵害しないこと、特定目的への適合、特定の提案、仕様、サンプルから生じる保
証を含むがこれに限定されないいかなる保証もなく「無保証で」提供されます。
本資料に掲載されている情報は、インテル製品の概要説明を目的としたものです。本資料は、明示されているか
否かにかかわらず、また禁反言によるとよらずにかかわらず、いかなる知的財産権のライセンスを許諾するため
のものではありません。製品に付属の売買契約書『Intel's Terms and conditions of Sales』に規定されている場合を
除き、インテルはいかなる責を負うものではなく、またインテル製品の販売や使用に関する明示または黙示の保
証(特定目的への適合性、商品性に関する保証、第三者の特許権、著作権、その他、知的所有権を侵害していな
いことへの保証を含む)にも一切応じないものとします。インテル製品は、医療、救命、延命措置などの目的へ
の使用を前提としたものではありません。
インテル製品は、予告なく仕様が変更される場合があります。
ハードウェア・ベンダの製品の設計、販売、機能に関する責任は、知的所有権の侵害または製品の保証から生じ
る責任を含めて、ハードウェア・ベンダのみが負うものとします。
性能に関するテストや評価は、一定のコンピュータ・システム、コンポーネント、またはそれらを組み合わせて
行ったものであり、このテストによるインテル製品の性能の概算の値を表しているものです。システム・ハード
ウェア、ソフトウェアの設計、構成等の違いにより、実際の性能は本サイトの性能テストや評価とは異なる場合
があります。システムやコンポーネントの購入を検討される場合は、ほかの情報も参考にして、パフォーマンス
を総合的に評価することをお勧めします。インテル製品の性能評価についてさらに詳しい情報をお知りになりた
い場合は、1-800-628-8686 または 1-916-356-3104(アメリカ合衆国)までご連絡ください。
インテル® Pentium® III Xeon™ プロセッサ、インテル® Pentium® 4 プロセッサ、インテル® Itanium® プロセッサは、
エラッタと呼ばれる設計上の不具合が含まれている可能性があり、公表されている仕様とは異なる動作をする場合
があります。そのようなエラッタは、インテルの保証範囲外です。現在確認済みのエラッタについては、インテル
までお問い合わせください。
* その他の社名、製品名などは、一般に各社の商標または登録商標です。
インテル、Intel ロゴ、Itanium、Pentium、VTune、Xeon は、アメリカ合衆国およびその他の国における Intel
Corporation またはその子会社の商標または登録商標です。
© 2003-2004, Intel Corporation. 無断での引用、転載を禁じます。
目次
第1章
概要 ........................................................................................................................... 1
目的 .................................................................................................................................................................. 1
前提知識........................................................................................................................................................... 1
記述範囲........................................................................................................................................................... 1
構成と執筆者 ................................................................................................................................................... 2
本シリーズの表記上の規則.............................................................................................................................. 2
第2章
2.1
2.2
2.3
2.4
2.5
インテル® C/C++ および Fortran コンパイラ .................................................................................................. 3
インテル® パフォーマンス・ライブラリ ......................................................................................................... 4
VTune™ パフォーマンス・アナライザ............................................................................................................ 4
インテル® スレッド・チェッカー.................................................................................................................... 4
インテル® スレッド・プロファイラ ................................................................................................................ 4
インテル® コンパイラによる自動並列化 ......................................................................................................... 5
インテル® マス・カーネル・ライブラリのマルチスレッド関数 ................................................................... 10
VTune™ パフォーマンス・アナライザによるスレッド間のフォルス・シェアリングの回避と特定 ............ 13
インテル® スレッド・チェッカーによるマルチスレッド・エラーの検出 .................................................... 18
スレッド・プロファイラによる OpenMP* パフォーマンスの評価 ............................................................... 23
第3章
3.1
3.2
3.3
3.4
3.5
3.6
3.7
3.8
3.9
4.3
4.4
4.5
同期 ......................................................................................................................... 71
ロックの競合と大小のクリティカル・セクションの管理 ............................................................................. 72
手作業でコーディングした同期ルーチンではなく、
スレッド関連の API が提供する同期ルーチンを使用する ............................................................................ 78
同期用の Win32* アトミック、ユーザ空間ロック、カーネル・オブジェクトの対比................................... 81
できるだけノンブロッキング・ロックを使用する........................................................................................ 85
ダブルチェック・パターンを使用して、1 回限りのイベントでのロック獲得を避ける ............................... 88
第5章
5.1
5.2
5.3
アプリケーションのスレッド化 .............................................................................. 29
適切なスレッド化手法の選択 : OpenMP* と明示的スレッド化 .................................................................... 30
粒度と並列性能.............................................................................................................................................. 36
ロード・バランスと並列性能 ........................................................................................................................ 41
ターンアラウンド重視のスレッド化とスループット重視の
スレッド化46
見かけの依存関係の回避または解消による並列性の露出 ............................................................................. 50
ワークロードの発見的手法による実行時の適切なスレッド数の決定 ........................................................... 54
スレッドプールによるシステム・オーバーヘッドの削減 ............................................................................. 57
順序付けされたデータ・ストリーム内のデータ並列性の利用...................................................................... 60
ループ・パラメータの操作による OpenMP* パフォーマンスの最適化........................................................ 66
第4章
4.1
4.2
インテル® ソフトウェア開発製品 ............................................................................. 3
メモリ管理............................................................................................................... 93
スレッド間のヒープの競合の回避 ................................................................................................................. 94
スレッドごとのローカル・ストレージによる同期の削減 ............................................................................. 98
スレッドスタックのオフセットによる、ハイパー・スレッディング・テクノロジ対応インテル®
プロセッサ上のキャッシュ競合の回避........................................................................................................ 103
i
図
図 1.
図 2.
図 3.
図 4.
図 5.
図 6.
図 7.
図 8.
図 9.
図 10.
図 11.
図 12.
図 13.
図 14.
ii
キャッシュ・ラインのフォルス・シェアリング ....................................................................................... 14
インテル® スレッド・チェッカー ............................................................................................................. 21
スレッド・プロファイラのサマリビュー.................................................................................................. 25
スレッド・プロファイラのリージョン・ビューと Legend ...................................................................... 26
スレッド・プロファイラのスレッドビュー .............................................................................................. 27
VTune™ アナライザのスレッド・プロファイラ表示 ............................................................................... 38
負荷の不均衡を示すタスク配分の例 ......................................................................................................... 42
ロード・バランスが改善されたタスク配分の例 ....................................................................................... 43
書き出し前のリオーダー・バッファの状態の例 ....................................................................................... 63
書き出し後のリオーダー・バッファの状態の例 ....................................................................................... 64
低いトリップカウントのループを並列化すると、負荷の不均衡が発生する............................................ 66
ネストされたループを結合してトリップカウントを増やすと、
並列性が顕在化され、パフォーマンスが向上する ................................................................................... 67
よく似たインデックスを持つ並列ループを融合すると、粒度とデータ局所性が向上する ...................... 68
インターロック関数とクリティカル・セクションの基本的な違い .......................................................... 84
表
表 2.1. 不正なロッキング階層を実行してもデッドロックが発生しない場合 ...................................................... 19
表 2.2. 不正なロッキング階層によるデッドロック ............................................................................................. 19
iii
コード例
コード例 1. OpenMP によって並列化された素数生成コード ............................................................. 37
コード例 2. critical プラグマの代わりに reduction 節を使用して
OpenMP によって並列化された素数生成コード ............................................................. 39
コード例 3. 3 × 3 のぼかしステンシルを記述する疑似コード ........................................................... 50
コード例 4. ループ内のポインタ・オフセット.................................................................................... 51
コード例 5. 異なる共有データのアップデートを保護する
2 つのクリティカル・セクションを含む、スレッド化された関数 .................................. 73
コード例 6. この関数によって使用されるすべての共有データのアップデートを保護する
1 つのクリティカル・セクションを含む、スレッド化された関数 .................................. 73
コード例 7. 1 つのクリティカル・セクションを 2 つに分割し、ロックの競合を減らしたコード ..... 74
コード例 8. 待機中のスレッドの動作を制御する発見的手法 .............................................................. 76
コード例 9. _alloca を使用したスレッドスタックのオフセットによって
キャッシュ競合を回避するコード.................................................................................. 106
iv
1
概要
目的
本シリーズ(本章「概要」を含めて全 5 章で構成)の目的は、インテル® アーキテクチャ・
ベースの対称型マルチプロセッサ(SMP)またはハイパー・スレッディング・テクノロジ対
応システム、あるいはその両方で動作する、効率的なマルチスレッド・アプリケーション開
発用のガイドラインを示すことである。アプリケーション開発者は、本シリーズに記載され
た推奨事項を利用して、
インテル® プロセッサで構成される現在および将来の SMP アーキテ
クチャ上でのマルチスレッド処理パフォーマンスを向上し、予想不可能なパフォーマンスの
変動を最小限に抑えられる。
本書は初版であり、マルチスレッド・アプリケーションのパフォーマンスに関する一般的な
推奨事項を示す。ハードウェア固有の最適化手法の説明は、最小限に抑えられている。将来
のバージョンでは、移植性を犠牲にしてもパフォーマンスを向上させたい場合のために、
ハードウェア固有の最適化手法の項目が追加される予定である。
前提知識
本書の読者は、高水準言語(できれば、C、C++、または Fortran)のプログラミング経験が
必要である。本書の推奨事項の多くは、Java*、C#、Perl などにも適用される。また、本書
の読者は、基本的なコンカレント・プログラミングについて理解し、1 つ以上のスレッド化
手法(できれば、OpenMP*、POSIX スレッド(Pthread とも呼ばれる)またはスレッド関連
の Win32* API)についてもよく理解している必要がある。
記述範囲
本書の主な目的は、インテル® プラットフォーム上のマルチスレッド・アプリケーションの
設計および最適化ガイドラインのクイック・リファレンスを提供することである。本書は、
マルチスレッド処理に関する教科書や、インテル・プラットフォームへの移植ガイドとして
の使用を前提としたものではない。
1
マルチスレッド・アプリケーションの開発
構成と執筆者
「プラットフォームに共通のスレッド化アプリケーションの開発」シリーズは、すべてのマ
ルチスレッド化手法に適用される一般的な推奨事項から、インテル® ソフトウェア製品向け
の使用ガイドラインと API 固有の問題までの項目を対象とする。各章はシリーズの一部を構
成するが、スレッド化に関する重要な問題の説明として個別に読むこともできる。
以下の表は、各章の内容と執筆者をまとめたものである。
章
記述範囲
執筆者
第 1 章「概要」
本シリーズの概要
Bill Magro
第 2 章「インテル® ソフト
この章は、インテル® ソフ
Bruce Greer、Clay
ウェア開発製品」
トウェア製品を使用したマ
Breshears、Judi Goldstein、
ルチスレッド・アプリケー
Martyn Corden、Phil Kerly、
ションの開発、デバッグ、
Vasanth Tovinkere
最適化の方法について説明
する。
第 3 章「アプリケーション
この章は、並列性能に関す
Aaron Coday、Bill Magro、
のスレッド化」
る一般的な事項を対象と
Clay Breshears、Henry
し、必要に応じて API 固
Gabb、Prasad
有の問題に言及する。
Kakulavarapu、Sanjiv Shah、
Vasanth Tovinkere
第 4 章「同期」
この章は、同期によるパ
Grant Haab、Henry Gabb、
フォーマンスへの悪影響を
Prasad Kakulavarapu、
緩和する手法について説明
Vasanth Tovinkere
する。
第 5 章「メモリ管理」
スレッドを利用することに
Clay Breshears、Jay
より、新たな次元のメモリ
Hoeflinger、Paul Petersen、
管理が必要になる。この章
Phil Kerly
は、マルチスレッド・アプ
リケーションに特有のメモ
リの問題を対象とする。
ユーザは、このシリーズ全体を無料でまとめてダウンロードすることも、必要に応じて各章
をダウンロードまたは通読することもできる。シリーズ全体を通して、関連項目への相互参
照が示される。
本シリーズの表記上の規則
「本シリーズ」は、上記の 5 章を指す。各章中の項目は、
「節」と呼ばれる。相互参照は、章
と節の番号を組み合わせた概略表記で示される。
2
2
インテル® ソフトウェア開発製品
インテル® ソフトウェア開発製品は、アプリケーションの迅速なスレッド化、デバッグの支
援、インテル・プロセッサ上でのマルチスレッド・アプリケーションのパフォーマンスの調
整を可能にする。ソフトウェア開発製品スイートは、各種のスレッド化手法をサポートして
いる。サポートしているスレッド化手法は、簡単な方法から順に、自動並列化、OpenMP* に
よるコンパイラ主導のスレッド化、Pthread およびスレッド関連の Win32* API などの標準ラ
イブラリを使用した手作業によるスレッド化である。
本節では、インテル・ソフトウェア開発スイートのコンポーネントを紹介する。そのため
に、各製品の概要を高い視点で把握し、製品の主な機能について説明する。インテル・ソフ
トウェア開発スイートは、以下の製品で構成される。
•
•
•
•
•
インテル® C/C++ および Fortran コンパイラ
インテル® パフォーマンス・ライブラリ
VTune™ パフォーマンス・アナライザ
インテル® スレッド・チェッカー
スレッド・プロファイラ
インテル・ソフトウェア開発製品の詳細は、http://www.intel.co.jp/jp/developer/software/products/
を参照のこと。
インテル・ソフトウェア・カレッジでは、すべてのインテル製品に関する講習とマルチス
レッド・プログラミングの教育を提供している。インテル・ソフトウェア・カレッジの詳細
は、https://shale.intel.com/softwarecollege/(英語)を参照のこと。
インテル® C/C++ および Fortran コンパイラ
インテル® コンパイラは、高水準コードの最適化以外に、自動並列化と OpenMP* のサポート
によるスレッド化も行える。コンパイラは、自動並列化によって、安全かつ効率的に並列実
行できるループを検出し、マルチスレッド化されたコードを生成する。プログラマは、
OpenMP によって、コンパイラ・ディレクティブと C/C++ プリプロセッサ・プラグマを使用
して並列性を表現できる。
3
マルチスレッド・アプリケーションの開発
インテル® パフォーマンス・ライブラリ
インテル® マス・カーネル・ライブラリ(インテル MKL)とインテル® インテグレーテッド・
パフォーマンス・プリミティブ(IPP)は、すべてのインテル® マイクロプロセッサ上で安定
したパフォーマンスを提供する。インテル MKL は、BLAS 関数、LAPACK 関数、およびベ
クトル数値演算関数をサポートしている。BLAS 関数において、レベル 2 およびレベル 3 の
すべての関数が OpenMP* によりスレッド化されている。IPP は、マルチメディア、オーディ
オ / ビデオ・コーデック、信号 / 画像処理、音声圧縮、およびコンピュータ・ビジョン用の
広範囲にわたるライブラリ関数と、数値演算サポート・ルーチンを提供している、クロスプ
ラットフォーム・ソフトウェア・ライブラリである。IPP は、インテル・マイクロプロセッ
サ向けに最適化されている。IPP を構成する関数の多くは、すでに OpenMP でスレッド化さ
れている。
VTune™ パフォーマンス・アナライザ
VTune™ パフォーマンス・アナライザは、開発者が、インテル® アーキテクチャ上で最適な
パフォーマンスを発揮するようにアプリケーションを調整するのを助ける。VTune パフォー
マンス・ツールは、インテル・マイクロプロセッサ内のイベントを監視し、アプリケーショ
ンの動作を詳細に表示する。これによって、パフォーマンス上のボトルネックを特定でき
る。VTune アナライザは、一定時間ごとのサンプリングとイベントごとのサンプリング、
コール・グラフ・プロファイリング、ホットスポット分析、チューニング・アシスタントな
ど、パフォーマンスのチューニングを支援する多くの機能を備えている。また、プロファイ
リング・データとソースコード内の正確な位置を関連付ける統合型ソースビューアも備えて
いる。
インテル® スレッド・チェッカー
インテル ® スレッド・チェッカーによって、ストレージの競合、デッドロック、API 違反、
矛盾した変数範囲、スレッドスタックのオーバーフローなどの一般的なエラーを自動的に検
出し、マルチスレッド・プログラムのデバッグを簡単に実行できる。並行実行性エラーは非
決定性のエラーであるため、従来のデバッガでは非常に見つけにくい。スレッド・チェッ
カーは、エラーの位置を関連するソース行のレベルでピンポイントで特定し、スレッドがエ
ラーに到達するまでにたどるパスを示すスタックトレースを表示する。また、スレッド・
チェッカーは、関連する変数も特定する。
インテル® スレッド・プロファイラ
インテル® スレッド・プロファイラによって、OpenMP* プログラムのチューニングが簡単に
行える。スレッド・プロファイラは、OpenMP 構文専用のパフォーマンス・カウンタを備え
ている。インテル・スレッド・プロファイラは、Serial リージョン、並列リージョン、クリ
ティカル・セクションでの所要時間の詳細を示し、負荷の不均衡、ロックの競合、並列オー
バーヘッドによるパフォーマンス上のボトルネックをグラフ表示する。パフォーマンス・
データは、プログラム全体、リージョンごと、個々のスレッドごとに表示できる。
4
インテル® ソフトウェア開発製品
2
2.1 インテル® コンパイラによる自動並列化
カテゴリ
ソフトウェア
記述範囲
対称型マルチプロセッサ(SMP)またはハイパー・スレッディング・テクノロジ(HT テク
ノロジ)対応システム、あるいはその両方で実行するために、インテル® コンパイラで作成
されたアプリケーション
キーワード
自動並列化、データの依存関係、プログラミング・ツール、コンパイラ
摘要
パフォーマンス向上のためにアプリケーションをマルチスレッド化するのは、時間のかか
る作業である。ほとんどの計算が単純なループで実行されるアプリケーションについては、
インテル・コンパイラを使用して、マルチスレッド・バージョンを自動的に生成できる。
背景情報
インテル® C++ および Fortran コンパイラは、ループ内のデータフローを分析し、安全かつ効
率的に並列実行できるループを判定する機能を持っている。自動並列化によって、SMP お
よび HT 対応システム上での実行時間が短縮される可能性がある。また、自動並列化によっ
て、プログラマは以下の作業から解放される。
•
•
•
並列実行の候補となるループを見つける
データフロー分析を実行し、適切な並列実行かどうかを検証する
並列コンパイラ・ディレクティブを手作業で追加する
プログラマに要求される操作は、コンパイル・コマンドに -Qparallel(Windows*)また
は -parallel(Linux*)オプションを追加するだけである。ただし、並列化が成功するか
どうかは、一定の条件に依存する。これについては、次の節で説明する。
以下の Fortran プログラムには、反復回数の多いループが含まれている。
PROGRAM TES
PARAMETER (N=100000000)
REAL A, C(N)
DO I = 1, N
A = 2 * I ? 1
C(I) = SQRT(A)
ENDDO
PRINT*, N, C(1), C(N)
END
5
マルチスレッド・アプリケーションの開発
データフロー分析によって、このループがデータの依存関係を含んでいないことが確認され
る。コンパイラは、実行時にできるだけ均等にスレッド間に反復を分割するコードを生成す
る。ス レ ッ ド の 数 は、デ フ ォ ル ト で は プ ロ セ ッ サ の 数 に 設 定 さ れ る が、環 境 変 数
OMP_NUM_THREADS によって独立して設定できる。並列化による特定のループの高速化の
度合は、作業量、スレッド間のロード・バランス、スレッドの作成と同期のオーバーヘッド
などによって異なるが、一般的にはスレッド数より小さくなる。
プログラム全体では、高速化の度合は、並列計算と逐次計算の比によって決まる(アムダー
ルの法則については、並列計算に関する教科書を参照)。
推奨事項
コンパイラがループを並列化するには、3 つの必要条件を満たしている必要がある。第 1 に、
前もって作業を分割できるように、ループに入る前に反復回数がわかっていなければならな
い。例えば、while ループは、通常は並列化できない。第 2 に、ループの中または外へのジャ
ンプがあってはならない。第 3 に、最も重要な条件として、ループの反復は互いに独立して
いなければならない。つまり、結果の正しさが、反復が実行される順序に論理的に依存して
いてはならない。ただし、例えば、同じ数量を異なる順序で加算した場合のように、累積丸
め誤差に多少の違いがあってもかまわない。特定の条件(配列の和を求める場合や、他の方
法で一時的なスカラ値を使用する場合)では、コンパイラが簡単な変換によって見かけの依
存関係を解消できるときがある。
ポインタまたは配列参照の潜在的な別名参照も、安全な並列化に対する一般的な障害とな
る。2 つのポインタが同じメモリ上の位置を指す場合、2 つのポインタは別名参照されてい
る。コンパイラは、例えば、2 つのポインタまたは配列参照が、関数の引数、ランタイム・
データ、または複雑な計算の結果に依存する場合は、それらが同じメモリ上の位置を指して
いるかどうかを判断できない。ポインタまたは配列参照が安全であり、反復が互いに独立し
ていることを証明できない場合、コンパイラはそのループを並列化しない(実行時に別名参
照について明示的にテストするための代替コード・パスを生成する価値があると判断される
場合を除く)。特定のループの並列化が安全であることをプログラマが知っており、潜在的
な別名参照を無視できる場合は、C プラグマ(#pragma parallel)または Fortran ディレ
クティブ(!DIR$ PARALLEL)によって、そのことをコンパイラに伝達できる。C 言語で
ポインタが別名参照されないように指示するもう 1 つの方法は、-Qrestrict(Windows)
または -restrict(Linux)コマンドライン・オプションと組み合わせて、ポインタ宣言内
で restrict キーワードを指定することである。コンパイラは、安全でない可能性のある
ループは並列化しない。
コンパイラは、比較的単純な構造を持つループだけを効果的に分析できる。例えば、コンパ
イラは、外部関数呼び出しを含むループのスレッド化の安全性を判断できない。これは、外
部関数呼び出しの副次的作用によって依存関係が発生するかどうかは、コンパイラにはわか
らないためである。Fortran 90 のプログラマは、PURE 属性を使用して、サブルーチンと関
数が副次的作用を含まないように指示できる。C または Fortran でのもう 1 つの方法は、
-Qipo(Windows)または -ipo(Linux)コンパイラ・オプションを指定して、プロシー
ジャ間の最適化を起動することである。この場合、コンパイラは、呼び出された関数に副次
的作用があるかどうかを分析する機会を与えられる。
6
インテル® ソフトウェア開発製品
2
プロ グラムが並列化し たいループを、コンパイラが自 動的に並列化でき ない場合は、
OpenMP* を使用できる。一般的に、OpenMP の使用を推奨する。これは、通常はプログラマ
がコンパイラよりコードをよく理解しており、粗い粒度で並列性を表現できるためである
(「アプリケーションのスレッド化」、3.2「粒度と並列性能」を参照)。一方、行列乗算内の
ループなど、ネストされたループについては、自動並列化が効果的である。外側のループの
スレッド化によって、適度な粗粒度の並列性が得られる。内側のループは、ベクトル化また
はソフトウェア・パイプライン化を使用して、細粒度の並列性が得られるように最適化でき
る。
あるループを並列化できるのは、そのループを並列化すべきであることを意味しない。した
がって、コンパイラは、しきい値パラメータを使用して、ループを並列化するかどうかを決
定する。-Qpar_threshold[n](Windows)および -par_threshold[n](Linux)コン
パイラ・オプションによって、このパラメータを調整できる。n の値の範囲は 0 ~ 100 であ
る。0 は、安全なループは常に並列化するという意味である。100 は、パフォーマンスが向
上する可能性の高いループだけを並列化するようにコンパイラに指示する。n のデフォルト
値は 75 である。
スイッチ -Qpar_report[n](Windows)または -par_report[n](Linux)(n の値は 1
~ 3)を使用して、どのループが並列化されたかを確認できる。次のようなメッセージが表
示される。
test.f90(6) : (col. 0) remark: LOOP WAS AUTO-PARALLELIZED
コンパイラは、並列化できなかったループとその理由もレポートする。
serial loop: line 6
flow data dependence from line 7 to line 8, due to “c”
以下のコード例の場合を考える。
void add (int k, float *a, float *b)
{
for (int i = 1; i < 10000; i++)
a[i] = a[i+k] + b[i];
}
コンパイラ・コマンド 'icl -c -Qparallel -Qpar_report3 add.cpp' を実行すると、
次のメッセージが返される。
add.cpp
procedure: add
serial loop: line 2
anti data dependence assumed from line 2 to line 2, due to "a"
flow data dependence assumed from line 2 to line 2, due to "a"
flow data dependence assumed from line 2 to line 2, due to "a"
7
マルチスレッド・アプリケーションの開発
コンパイラは、k の値を知らないため、例えば k = -1 の場合のように、反復が互いに依存す
ると見なさなければならない。しかし、プログラマには、アプリケーションに関する具体的
な知識(例えば、k は常に 10000 より大きい)があり、反復が互いに独立していることを
知っている。この場合、プログラマは、次のプラグマを挿入すれば、コンパイラの動作を変
更できる。
void add (int k, float *a, float *b)
{
#pragma parallel
for (int i = 1; i < 10000; i++)
a[i] = a[i+k] + b[i];
}
この場合は、ループが並列化されたことを示すメッセージが返される。
add.cpp
add.cpp(3) : (col. 3) remark: LOOP WAS AUTO-PARALLELIZED.
procedure: add
parallel loop: line 3
shared: {"b", "a", "k"}
private: {"i"}
first private: { }
reductions: { }
ただし、この場合、k の値が 10000 より小さい状態で関数を呼び出さないのは、プログラマ
の責任となる。k の値が 10000 より小さい状態でこの関数を呼び出すと、不適当な結果が返
されるときがある。
使用ガイドライン
-parallel(Linux)または -Qparallel(Windows)コンパイラ・スイッチを指定して、
大量の計算を必要とするアプリケーション・カーネルを構築してみる。-par_report3
(Linux)または -Qpar_report3(Windows)オプションを指定してレポート機能を有効に
し、どのループが並列化され、どのループが並列化できなかったかを確認する。並列化でき
なかったループについて、データの依存関係を解消するか、別名参照の可能性があるメモリ
参照をコンパイラに指示する。
ループを並列化するのに必要な変換が、他のハイレベルの最適化手法(例えば、ループの反
転)に影響を与えるときがある。この影響は、多くの場合、コンパイラの最適化レポートか
ら認識できる。並列化を使用した場合と使用しない場合のパフォーマンスを常に測定し、有
効な高速化が実現されているかどうかを確認する必要がある。
同じコマンドライン上に -openmp と -parallel の両方を指定した場合、コンパイラは、
OpenMP ディレクティブを含まない関数だけを自動的に並列化しようとする。
別々のコンパイル手順とリンク手順を持つ構築プロセスで、自動並列化を使用する場合は、
必ず OpenMP ランタイム・ライブラリをリンクする。OpenMP ライブラリをリンクする最も
簡単な方法は、リンク用のコンパイラ・ドライバ(例えば、IA-32 プロセッサと Windows で
は icl -Qparallel、インテル® Itanium® プロセッサと Linux では efc -parallel)を使
用することである。
8
インテル® ソフトウェア開発製品
2
参考資料
本シリーズの参照個所 :
本章、2.2「インテル® マス・カーネル・ライブラリのマルチスレッド関数」
本章、2.4「スレッド・プロファイラによる OpenMP パフォーマンスの評価」
アプリケーションのスレッド化、3.2「粒度と並列性能」
同期、3.5「見かけの依存関係の回避または解消による並列性の露出」
他の参考資料 :
『インテル® C++ コンパイラ・ユーザーズ・ガイド』または『インテル® Fortran
コンパイラ・ユーザーズ・ガイド』
、「コンパイラによる最適化 / 並列化 / 自動
並列化」を参照。
「Efficient Exploitation of Parallelism on Pentium® III and Pentium 4 Processor-Based
Systems」
、Aart Bik、Milind Girkar、Paul Grey 、Xinmin Tian 共著、Intel
Technology Journal
http://www.intel.com/technology/itj/q12001/articles/art_6.htm
インテル・ソフトウェア・カレッジでは、インテル・ソフトウェア開発製品に
関する広範囲にわたる研修教材を提供している。
9
マルチスレッド・アプリケーションの開発
2.2 インテル® マス・カーネル・ライブラリのマルチスレッド関数
カテゴリ
ソフトウェア
記述範囲
Windows* または Linux* オペレーティング・システムを実行する、インテル® Pentium® プロ
セッサからインテル® Xeon™ プロセッサまでの 32 ビット・プロセッサとインテル® Itanium®
プロセッサ・ファミリに適用される。
キーワード
マス・カーネル・ライブラリ、BLAS、LAPACK、FFT、プログラミング・ツール
摘要
共有メモリ環境のマルチプロセッサ搭載システム上のパフォーマンスを向上させるために、
インテル® マス・カーネル・ライブラリ(インテル MKL)内の多くの主要ルーチンがスレッ
ド化されている。このライブラリを使用して、シングル・プロセッサ・システムとマルチプ
ロセッサ・システムの両方で、主要なアルゴリズムの高性能が簡単な方法で得られる。ユー
ザは、使用するプロセッサの数をシステムに指示するだけで良い。
背景情報
多くの科学計算コードは並列化可能であるが、すべてのコードが SMP システムの複数のプ
ロセッサ上で高速化されるわけではない。これは、科学計算をサポートするのに十分なメモ
リ帯域幅が存在しないためである。幸いなことに、財務会計、エンジニアリング、科学分野
の技術計算の重要な要素は、キャッシュを効果的に使用できる算術演算に依存しているた
め、メモリシステムへの要求は軽減される。1 つのタスクで複数のプロセッサを効果的に使
用するための基本的な条件は、キャッシュ内のデータの再利用率が十分に高く、メモリバス
が他のプロセッサのために解放されることである。密度の高い行列の因数分解や行列の乗算
(因数分解の主な要素)などの演算は、演算が適切に構造化されている場合は、この条件を
満たせる。
単にコードをコンパイルすることで(多くの場合、ハイレベルのコード最適化手法を組み合
わせる)、プロセッサのピーク・パフォーマンスに近い性能が得られることもある。しかし、
コンパイルされたコードがメモリ帯域幅に大きく依存する場合は、コードが並列化されて
も、恐らく十分に高速化されない。これは、キャッシュ利用率が不十分なため、すべてのプ
ロセッサにデータを供給するにはメモリ帯域幅が不足するためである。
BLAS(基本線形代数サブルーチン)のレベル 3 関数(すべての行列 - 行列演算)、多くの
LAPACK(線形代数パッケージ)関数、DFT(離散フーリエ変換)関数などの広く使用され
ている関数は、すべてキャッシュ内のデータを十分に再利用できるため、メモリバス上で複
数のプロセッサをサポートできる。
10
インテル® ソフトウェア開発製品
2
推奨事項
推奨事項には、実際には 2 つの部分がある。第 1 に、ユーザは、広く使用されていて、事実
上の標準関数である BLAS と LAPACK をできるだけ使用するべきである。これらの関数は
ユーザが構築しているソースコードから利用でき、多くのハードウェア・ベンダが自社のマ
シン向けに最適化した関数バージョンを提供している。単に高性能ライブラリにリンクする
ことで、アプリケーションのパフォーマンスが大きく向上する可能性がある。パフォーマン
スがどの程度向上するかは、アプリケーションがどの程度 LAPACK に(つまり、暗黙的に
BLAS に)依存しているかによって異なる(LAPACK は BLAS に基づいて作成されている)
。
インテル® MKL は、BLAS および LAPACK 関数を含むインテルのライブラリである。レベ
ル 3 BLAS は、シングルプロセッサで高性能が得られるようにチューニングされているが、
マルチプロセッサでも実行でき、2 個以上のプロセッサの使用時に十分に高速化されるよう
にスレッド化されている。LAPACK の主要な関数もスレッド化されている。スレッド化され
た BLAS を使用するだけで、マルチプロセッサ上で高性能が得られるが、LAPACK をスレッ
ド化すると、より小規模な問題のパフォーマンスが向上する。LINPACK ベンチマーク(一
連の方程式を解くテスト)は、これらの関数のスレッド化によって得られる高速化を実証し
ている。このベンチマークは、LAPACK から 2 つの高水準関数(因数分解ルーチンと解の算
出ルーチン)を使用する。ほとんどの時間は因数分解に使われる。最大のテスト問題では、
インテルMKL は、4 プロセッサ上で 3.84 倍の高速化(96% の並列実行効率)を達成した。
BLAS および LAPACK のスレッド化ルーチン以外に、DFT もスレッド化され、マルチプ
ロセッサで大幅に高速化されている。例えば、1280 × 1280 の単精度複素 2D 変換の場合、
インテル® Itanium® 2 プロセッサ上でのパフォーマンスは、1 プロセッサ、2 プロセッサ、
4 プロセッサで、それぞれ 1908MFLOPS、3225MFLOPS(1.69 倍の高速化)、7183MFLOPS
(3.76 倍の高速化)である。
使用ガイドライン
インテル® MKL の現在のリリース(バージョン 6.1 まで)では、これらの関数の使用につい
て、マス・カーネル・ライブラリには直接関係のない注意事項が存在する。環境によって
は、問題が発生する場合がある。
インテル MKL のスレッド化には、OpenMP が使用されている。また、インテル MKL は、
インテル・コンパイラと同じ OpenMP ランタイム・ライブラリを使用する。従って、インテル
MKL を使用する OpenMP アプリケーションがインテル・コンパイラでコンパイルされていない
場合は、問題が発生するときがある。厳密には、このアプリケーションは、2 つの異なる OpenMP
ライブラリを使用しようとする。1 つは非インテル・コンパイラの OpenMP ライブラリであり、
もう 1 つはインテル MKL の OpenMP ライブラリである。環境変数 OMP_NUM_THREADS の値が
1 より大きい場合、両方のライブラリがスレッドを作成しようとすると、混乱が生じてプログ
ラムは失敗する。インテル MKL の将来のバージョンでは、スレッド作成を制御する別の手段
を用意する予定である。それまでの間、この問題が検出された場合は、インテル・プレミア・
サポート http://premier.intel.com/(英語)を通じてこの問題をインテルに報告すると、暫定的な
解決策が提供される。
11
マルチスレッド・アプリケーションの開発
第 2 の問題は、対称型マルチプロセッサ・ノード1 を含むクラスタ上で発生する。このよう
なクラスタ上で MPI または PVM アプリケーションを実行すると、多くの場合、ノード内の
各プロセッサにつき 1 つのプロセスが生成される。ノードは、オペレーティング・システ
ム・イメージを持つコンピュータとして定義される。一般的なクラスタでは、オペレーティ
ング・システムはクラスタ内の各コンピュータにインストールされる。MPI または PVM ア
プリケーションがインテル MKL を使用する場合、各 MPI または PVM プロセスもスレッド
を作成する。その結果、ノード内のプロセッサ・リソースの衝突が発生する。1 プロセッサ
当たり 1 プロセスを生成する MPI または PVM アプリケーションでは、OMP_NUM_THREADS
を 1 に設定することを推奨する。
参考資料
本章の参照個所 :
2.1「インテル® コンパイラによる自動並列化」
2.5「スレッド・プロファイラによる OpenMP パフォーマンスの評価」
他の参考資料 :
インテル・マス・カーネル・ライブラリは、
http://www.intel.co.jp/jp/developer/software/products/perflib/ で入手できる。
インテル・ソフトウェア・カレッジでは、インテル・ソフトウェア開発製品に
関する広範囲にわたる研修教材を提供している。
BLAS と LAPACK については、http://www.netlib.org(英語)を参照のこと。
1.
12
ノードは、オペレーティング・システム・イメージを持つコンピュータとして定義される。一般的なクラス
タでは、オペレーティング・システムはクラスタ内の各コンピュータにインストールされる。
インテル® ソフトウェア開発製品
2
2.3 VTune™ パフォーマンス・アナライザによるスレッド間のフォ
ルス・シェアリングの回避と特定
カテゴリ
ソフトウェア
記述範囲
一般的なマルチスレッド処理
キーワード
VTune™ アナライザ、キャッシュ・コヒーレンシ、データ・アライメント、プロファイラ、
プログラミング・ツール
摘要
対称型マルチプロセッサ(SMP)システム内では、各プロセッサがローカル・キャッシュを
持つ。従って、メモリシステムは、キャッシュ・コヒーレンシを保証しなければならない。
フォルス・シェアリングは、異なるプロセッサ上のスレッドが、同じキャッシュ・ライン上
に置かれた異なる変数を変更したときに発生する。それぞれの書き込みによって、他の
キャッシュ内のラインが無効化されるため、キャッシュが強制的に更新され、パフォーマン
スが低下する。この項目では、インテル® VTune™ パフォーマンス・アナライザを使用して
フォルス・シェアリングを検出し、修正する方法について説明する。
背景情報
フォルス・シェアリングは、各プロセッサがローカル・キャッシュを持つ SMP 上で発生す
る、周知のパフォーマンス上の問題である。フォルス・シェアリングは、図 1 に示すよう
に、異なるプロセッサ上のスレッドが、同じキャッシュ・ライン上に置かれた変数を変更し
たときに発生する。この状態がフォルス・シェアリングと呼ばれるのは、各スレッドは同じ
変数へのアクセスを実際に共有していないからである。同じ変数へのアクセス(すなわち、
真の共有)を行うには、プログラム上の同期構文を使用して、順序付けされたデータアクセ
スを保証する必要がある。
13
マルチスレッド・アプリケーションの開発
以下のコード例で、下線でハイライトされているソース行は、フォルス・シェアリングの原
因になる。
double sum=0.0, sum_local[NUM_THREADS];
#pragma omp parallel num_threads(NUM_THREADS)
{
int me = omp_get_thread_num();
sum_local[me] = 0.0;
#pragma omp for
for (i = 0; i < N; i++)
sum_local[me] += x[i] * y[i];
#pragma omp atomic
sum += sum_local[me];
}
配列 sum_local でフォルス・シェアリングが発生する可能性がある。この配列は、スレッ
ドの数に従ってサイズが決められ、1 つのキャッシュ・ラインに納まる程度に小さい。この
コードを並列実行すると、各スレッドは sum_local の異なる(隣接する)要素を変更する
(下線でハイライトされているソース行)
。これによって、すべてのプロセッサについて、こ
のキャッシュ・ラインが無効化される。
図 1.
キャッシュ・ラインのフォルス・シェアリング
ࠬ࡟࠶࠼
ࠬ࡟࠶࠼
CPU 0
CPU 1
2.
3.
ࠠࡖ࠶ࠪࡘ࡮࡜ࠗࡦ
ࠠࡖ࠶ࠪࡘ࡮࡜ࠗࡦ
ࠠࡖ࠶ࠪࡘ
1.
ࠠࡖ࠶ࠪࡘ
1.
ࡔࡕ࡝
14
インテル® ソフトウェア開発製品
2
フォルス・シェアリングは、異なるプロセッサ上のスレッドが、同じキャッシュ・ライン上に
置かれた変数を変更したときに発生する。これによって、キャッシュ・コヒーレンシを維持す
るためにキャッシュ・ラインが無効化され、メモリが強制的に更新される。この状態を上図に
示す。スレッド 0 とスレッド 1 は、メモリ内で互いに隣接する、同じキャッシュ・ライン上に
置かれた変数を要求する。このキャッシュ・ラインは、CPU0 と CPU1 のキャッシュ内にロー
ドされる(グレーの矢印 1.)。各スレッドは異なる変数を変更しているが(赤い矢印 2. と青い
矢印 3.)、キャッシュ・ラインは無効化される。これによって、キャッシュ・コヒーレンシを維
持するために、メモリが強制的に更新される。
複数のキャッシュにわたるデータの整合性を保証するために、インテルのマルチプロセッサ
対応プロセッサは、MESI(Modified/Exclusive/Shared/Invalid)プロトコルに従う。プロセッ
サは、キャッシュ・ラインを最初にロードしたとき、そのキャッシュ・ラインを 'Exclusive'
(排他)アクセスとしてマークする。キャッシュ・ラインが Exclusive としてマークされてい
る間は、これ以降のロードでキャッシュ内の既存のデータを自由に使用できる。このプロ
セッサは、バス上の他のプロセッサが同じキャッシュ・ラインをロードしたことを認識する
と、このキャッシュ・ラインを 'Shared'(共有)アクセスとしてマークする。このプロセッ
サが 'S' としてマークされたキャッシュ・ラインを格納すると、キャッシュ・ラインは
'Modified'(変更)としてマークされ、他のすべてのプロセッサに 'Invalid'(無効)キャッ
シュ・ライン・メッセージが送信される。このプロセッサは、ここで 'M' としてマークされ
た同じキャッシュ・ラインに他のプロセッサがアクセスしていることを認識すると、キャッ
シュ・ラインをメモリに書き戻し、'Shared' としてマークする。同じキャッシュ・ラインに
アクセスしている他のプロセッサは、キャッシュ・ミスになる。
キャッシュ・ラインが 'Invalid' としてマークされると、プロセッサ間の頻繁な調整が必要に
なり、メモリへのキャッシュ・ラインの書き込みとその後のロードが要求される。フォル
ス・シェアリングがあると、この調整プロセスが増加し、アプリケーションのパフォーマン
スが大きく低下する場合がある。
推奨事項
本節の基本的な推奨事項は、マルチスレッド・アプリケーション内のフォルス・シェアリン
グを避けることである。ただし、すでに存在するフォルス・シェアリングの検出は、別の問
題である。第 1 の検出方法は、コードの点検である。グローバルまたは動的に割り当てられ
た共有データ構造に複数のスレッドがアクセスするインスタンスを探す。これらのインスタ
ンスは、フォルス・シェアリングの潜在的な原因となる。ただし、全く異なるグローバル変
数がメモリ内で偶然比較的近くに置かれたとき、それらの変数に複数のスレッドがアクセス
する場合がある。このようなフォルス・シェアリングは見付けにくい。スレッドごとのロー
カル・ストレージやローカル変数を、フォルス・シェアリングの原因として禁止できる。
15
マルチスレッド・アプリケーションの開発
これより良い検出方法は、インテル® VTune™ パフォーマンス・アナライザを使用すること
で あ る。マ ル チプ ロ セッ サ・シス テ ムの 場 合は、'2nd Level Cache Load Misses
Retired' イベントをサンプリングするように、VTune アナライザを設定する。ハイパー・
スレッディング・テクノロジ対応プロセッサの場合は、'Memory Order Machine Clear' イベン
トをサンプリングするように、VTune アナライザを設定する。これらのイベントが、スレッ
ド内のロード / ストア命令またはその近くで高頻度で集中して発生する場合は、フォルス・
シェアリングが存在する可能性がある。コードを点検して、異なるメモリ上の位置が同じ
キャッシュ・ライン上に置かれていないかどうか確認する。
検出されたフォルス・シェアリングを修正するには、いくつかの方法がある。目標は、フォ
ルス・シェアリングの原因となる変数が同じキャッシュ・ライン上に置かれないように、メ
モリ内で十分な距離を保証することである。ここではすべての可能な方法は説明できない
が、3 つの方法を以下に説明する。
1 つの方法は、インテル・コンパイラ 7.0 以降または Microsoft Visual Studio* .NET のディレ
クティブを使用して、強制的に個々の変数のアライメントを合わせることである。以下の
ソースコードは、コンパイラの '__declspec (align(n))' 文(n の値は 16(128 バイト
境界))を使用して、個々の変数をキャッシュ・ライン境界上にアライメントする方法を示
している。ただし、コンパイラの中には、この手法による 128 バイト・アライメントをサ
ポートしないものもある。
__declspec (align(128)) int thread1_global_variable;
__declspec (align(128)) int thread2_global_variable;
データ構造体の配列を使用する場合は、配列要素がキャッシュ・ライン境界上で始まるよう
に、キャッシュ・ラインの終わりに合わせてデータ構造体をパディングする。キャッシュ・
ライン境界上の配列のアライメントを保証できない場合は、キャッシュ・ラインのサイズの
2 倍に合わせてデータ構造体をパディングする。以下のソースコードは、コンパイラの
'__declspec (align(n))' 文(n の値は 16(128 バイト境界)
)を使用して、キャッシュ・
ライン境界に合わせてデータ構造体をパディングし、配列のアライメントも保証する方法を
示している。配列が動的に割り当てられる場合は、割り当てサイズを大きくして、キャッ
シュ・ライン境界にアライメントされるようにポインタを調整できる。
struct ThreadParams
{
// For the following 4 variables: 4*4 = 16 bytes
unsigned long thread_id;
unsigned long v; // Frequent read/write access variable
unsigned long start;
unsigned long end;
// expand to 128 bytes to avoid false-sharing
// (4 unsigned long variables + 28 padding)*4 = 128
int padding[28];
};
__declspec (align(128)) struct ThreadParams Array[10];
16
インテル® ソフトウェア開発製品
2
また、データのスレッドごとのローカルコピーを使用すると、フォルス・シェアリングの頻
度を下げられる。スレッドごとのローカルコピーは、頻繁に読み取りと変更が行え、完了し
た時点で結果がデータ構造体に書き戻される。以下のソースコードは、ローカルコピーを使
用してフォルス・シェアリングを避ける方法を示している。
struct ThreadParams
{
// For the following 4 variables: 4*4 = 16 bytes
unsigned long thread_id;
unsigned long v;
//Frequent read/write access variable
unsigned long start;
unsigned long end;
};
void threadFunc(void *parameter)
{
ThreadParams *p = (ThreadParams*) parameter;
// local copy for read/write access variable
unsigned long local_v = p->v;
for(local_v = p->start; local_v < p->end; local_v++)
{
// Functional computation
}
p->v = local_v; // Update shared data structure only once
}
使用ガイドライン
フォルス・シェアリングを避けることと、これらの手法はできるだけ使わないことを推奨す
る。必要以上にこれらの手法を使用すると、プロセッサの利用可能キャッシュを効果的に使
用できなくなる。
マルチプロセッサ共有型キャッシュ設計でも、フォルス・シェアリングを避けることを推奨
する。一般的に、マルチプロセッサ共有型キャッシュ設計でキャッシュ利用率を最大限に上
げることから得られる多少の高速化のメリットより、異なるキャッシュ・アーキテクチャ向
けに複数のコードパスをサポートするのに必要なソフトウェア保守コストの負担の方が大
きい。
参考資料
本シリーズの参照個所 :
本章、2.5「スレッドプロファイラによる OpenMP パフォーマンスの評価」
メモリ管理、5.3「スレッドスタックのオフセットによるハイパー・スレッ
ディング・テクノロジ対応インテル® プロセッサ上でのキャッシュの競合の回
避」
他の参考資料 :
インテル・ソフトウェア・カレッジでは、インテル・ソフトウェア開発製品に
関する広範囲にわたる研修教材を提供している。本項目については、オンライ
ン・コース「VTune™ パフォーマンス・アナライザ入門」の受講をお勧めす
る。
17
マルチスレッド・アプリケーションの開発
2.4 インテル® スレッド・チェッカーによるマルチスレッド・エラー
の検出
カテゴリ
ソフトウェア
記述範囲
自動化された Win32* 環境におけるマルチスレッド・アプリケーションのデバッグ
キーワード
スレッド・チェッカー、VTune™ アナライザ、デバッガ、プログラミング・ツール、レース状態
摘要
インテル・スレッド化ツールの 1 つであるインテル ® スレッド・チェッカーを使用して、
Win32 アプリケーションのマルチスレッド・エラーをデバッグできる。スレッド・チェッ
カーは、OpenMP* とマルチスレッド関連の Win32 API をサポートしている。スレッド・
チェッカーは、ストレージの競合、デッドロック、デッドロックの原因となる状態、スレッ
ドのストール、放棄されたロックなどを自動的に検出する。
背景情報
マルチスレッド・プログラムは時間的なコンポーネントを含むため、逐次実行プログラムよ
りデバッグが難しい。並行実行性エラー(例えば、データの競合やデッドロック)は非決定
性のエラーであるため、発見して再現することが難しい。プログラマの運が良ければ、この
エラーは常にプログラムのクラッシュやデッドロックを引き起こす。しかし、それほど運が
良くない場合は、99% の時間は正常に動作するが残りの 1% の時間に異常が発生したり、エ
ラーによって生じたわずかな数値のずれが、長時間の実行後に明らかになる。
従来のデバッグ手法では、マルチスレッド・プログラムには対応できない。デバッグ・プ
ローブ(すなわち、print 文)は、多くの場合、マルチスレッド・プログラムのタイミングを
変更してエラーを隠してしまう。バグが安定して再現される場合は、デバッガ内でマルチス
レッド・プログラムを実行すると、ある程度の情報が得られる。ただし、プログラマは、エ
ラーを診断するために、複数のスレッド状態(すなわち、命令ポインタ、スタック)をチェッ
クしなければならない。
インテル・スレッド・チェッカーは、マルチスレッド・プログラムのデバッグ専用に設計さ
れている。スレッド・チェッカーは、次のように、ほとんどの一般的な並行プログラミン
グ・エラーを検出し、プログラム内のエラーの位置をピンポイントで特定する。
•
18
ストレージの競合 - 最も一般的な並行実行性エラーは、共有データの変更動作が同期し
ていないことから発生する。例えば、複数のスレッドが同じ静的変数を同時にインクリ
メントした場合、データが失われても、プログラムがクラッシュする可能性は小さい。
次の節では、インテル・スレッド・チェッカーを使用してこのようなエラーを検出する
方法について説明する。
インテル® ソフトウェア開発製品
•
2
デッドロック - スレッドが、いつまでも発生しないリソースまたはイベントを待機しな
ければならない状態を、デッドロックと呼ぶ。デッドロックの一般的な原因は、不正な
ロッキング階層である。例えば、あるスレッドがロック A とロック B をこの順序で獲
得しようとしたとき、他のスレッドが同じロックを逆の順序で獲得しようとした場合を
考える。このコードを実行しても、デッドロックが発生しない場合がある(表 2.1)。
表 2.1. 不正なロッキング階層を実行してもデッドロックが発生しない場合
時間
スレッド 1
T0
ロック A を獲得
T1
ロック B を獲得
T2
タスクを実行
T3
ロック B を解放
T4
ロック A を解放
スレッド 2
T5
ロック A を獲得
T6
ロック B を獲得
T7
タスクを実行
T8
ロック B を解放
T9
ロック A を解放
しかし、このロッキング階層が両方のスレッドのデッドロックを発生させることもある(表
2.2)。両方のスレッドは、いつまでも獲得できないリソースを待機している。スレッド・
チェッカーは、デッドロック、潜在的なデッドロック、および競合しているリソースを特定
する。
表 2.2.
不正なロッキング階層によるデッドロック
時間
スレッド 1
T0
ロック A を獲得
T1
ロック B を獲得
T2
ロック A を待機
T3
•
スレッド 2
ロック B を待機
放棄されたロック - Win32 クリティカル・セクションまたは mutex 変数を保持している
間にスレッドが終了すると、デッドロックまたは予想不可能な動作の原因となる。従っ
て、スレッド・チェッカーはこの状態を検出する。放棄されたクリティカル・セクショ
ンを待機しているスレッドは、デッドロックになる。放棄された mutex はリセットされ
る。
19
マルチスレッド・アプリケーションの開発
•
失われた信号 - Win32 イベントを待機しているスレッドが存在しないときに、その
Win32 イベントの変数がパルスされた場合は(すなわち、Win32 PulseEvent 関数)、
デッドロックの一般的な兆候と見なされる。従って、スレッド・チェッカーは、この状
態を検出する。例えば、プログラマは、イベントがパルスされる前にスレッドが待機し
ていると予想する。
スレッドがイベントを待機する前にイベントがパルスされると、そのスレッドは、いつ
までも到着しない信号を待機する場合がある。
スレッド・チェッカーは、これ以外にも多くのエラーを検出する。これには、API 使用違反、
スレッドスタックのオーバーフロー、有効範囲違反などが含まれる。
推奨事項
インテル・スレッド・チェッカーを使用して、OpenMP および Win32 マルチスレッド・アプ
リケーションのデバッグを簡単に実行できる。マルチスレッド・プログラムのエラーは、逐
次実行プログラムのエラーより見付けにくい。その理由として、マルチスレッド・プログラ
ムには、上で説明した時間的なコンポーネントが含まれる。また、マルチスレッド・プログ
ラムのエラーは 1 個所に限定されない。プログラムの別々の部分で動作する複数のスレッド
が、エラーを発生させるときがある。以下の簡単な例に示すように、スレッド・チェッカー
によって、デバッグ時間を大幅に短縮できる。
スレッド・チェッカー分析用のプログラムを準備するために、最適化を無効、デバッグシン
ボルを有効に設定して、プログラムをコンパイルする。実行ファイルを再配置できるよう
に、/fixed:no オプションを指定して、プログラムをリンクする。スレッド・チェッカー
は、VTune™ パフォーマンス・アナライザ(インテルのパフォーマンス・チューニング環
境)内で実行した場合、得られた実行ファイル・イメージをインストルメントする。バイナ
リ・インストルメーションの場合は、Microsoft* Visual C++* コンパイラ(バージョン 6.0 以
降)または インテル® C++ および Fortran コンパイラ(バージョン 7.0 またはそれ以降)を使
用できる。インテル・コンパイラは、ソースレベルのインストルメーション(/Qtcheck オ
プション)をサポートしており、より詳細な情報が得られる。
次のプログラムには、小さな競合状態が含まれている。
このプログラムは、それぞれの識別番号をレポートする 4 つのスレッドを作成する。このプ
ログラムは、通常は、次のような予想どおりの出力を生成する。
Thread
Thread
Thread
Thread
0
1
2
3
reporting
reporting
reporting
reporting
各スレッドは、常に識別番号の順にレポートするとは限らないが、すべてのスレッドがメッ
セージを出力する。しかし、場合によっては、次のように、一部のスレッドは 2 回以上レ
ポートし、他のスレッドは全くレポートせず、不可解な新しいスレッドが現れる。
Thread
Thread
Thread
Thread
20
2
3
3
4
reporting
reporting
reporting
reporting
インテル® ソフトウェア開発製品
2
スレッド・チェッカーは、このプログラム内のエラーを簡単に検出し、エラーの原因となる
文を表示する(図 2)
。
図 2.
インテル® スレッド・チェッカー
1.
エラーの説明(赤いボックス 1. を参照)は、ストレージの競合についてわかりやすい英語で説
明している。つまり、あるスレッドが行 7 の変数 my_id を読み込んでいるとき、他のスレッド
が同時に行 15 の変数 id に書き込んでいる。関数 ReportID 内の変数 my_id は、変数 id へのポ
インタであるが、変数 id はメインルーチン内で変更されている。プログラマは、スレッドが作
成された時点でスレッドの実行が開始されると間違って考えている。しかし、オペレーティン
グ・システムがスレッドをスケジューリングする順序に制限はない。メインスレッドがすべて
のワーカスレッドを作成した後に、一部のワーカスレッドの実行が開始されることもある。こ
のエラーを修正するには、変更されない独自の位置へのポインタを、各スレッドに渡せば良い。
使用ガイドライン
インテル® スレッド・チェッカーは、現在のところ、32 ビット Microsoft Windows* 2000 およ
び Windows* XP オペレーティング・システム対応版が提供されている。スレッド・チェッ
カーは、OpenMP とスレッド関連の Win32 API の両方をサポートしている。OpenMP のサ
ポートには、インテル・コンパイラが必要である。また、詳細なソースレベルのインストル
メーションにも、インテル・コンパイラが必要である。これらが不要な場合は、Microsoft
Visual C++ コンパイラを使用できる。
21
マルチスレッド・アプリケーションの開発
ただし、インテル・スレッド・チェッカーは、静的分析ではなく、動的分析を実行する。ス
レッド・チェッカーは、実行されるコードだけを分析する。従って、コードの分析範囲に漏
れがないように、複数の分析によってプログラムの異なる部分を実行する必要がある。
スレッド・チェッカーによるインストルメーションは、アプリケーションに必要な CPU お
よびメモリ環境を増大させる。従って、小さくて代表性のあるテスト問題の選択が、非常に
重要である。実行時間が数秒で終わるワークロードが最適である。ワークロードは、現実的
なものである必要はなく、マルチスレッド・コードの適切なセクションをテストできれば良
い。例えば、画像処理アプリケーションをデバッグする場合、スレッド・チェッカーの分析
には 10 × 10 ピクセルの画像で十分である。大きい画像を使用しても、分析に時間がかかる
だけで、追加情報はほとんど得られない。同様に、マルチスレッド化されたループをデバッ
グする場合は、反復回数を小さくすることを推奨する。
参考資料
インテル® スレッド・チェッカーの Web サイト
『Getting Started with the Intel Threading Tools』(インテル・スレッド化ツールに
同梱)
『Intel Thread Checker Lab』(インテル・スレッド化ツールに同梱)
インテル・ソフトウェア・カレッジでは、インテル・ソフトウェア開発製品に
関する広範囲にわたる研修教材を提供している。本項目については、オンライ
ン・コース「インテル・スレッド化ツールの使用」の受講をお勧めする。
22
インテル® ソフトウェア開発製品
2
2.5 スレッド・プロファイラによる OpenMP* パフォーマンスの評価
カテゴリ
ソフトウェア
記述範囲
Windows* プラットフォーム上の OpenMP* パフォーマンスのチューニング
キーワード
プロファイラ、プログラミング・ツール、OpenMP、VTune™ アナライザ、並列オーバーヘッド
摘要
スレッド・プロファイラは、インテル® スレッド化ツールの 1 つである。スレッド・プロファ
イラは、OpenMP でスレッド化されたコードのパフォーマンスの評価、パフォーマンス上の
ボトルネックの特定、OpenMP アプリケーションのスケーラビリティの測定に使用できる。
スレッド・プロファイラ 2.0 では、Win32 のクリティカル・セクションにも対応している。
背景情報
アプリケーションのデバッグが完了し、正常に動作するようになった段階で、技術者は通
常、パフォーマンス・チューニングを開始する。しかし、従来のプロファイラは、OpenMP
構文を認識できない、負荷の不均衡をレポートできない、同期オブジェクトの競合をレポー
トしないなどの様々な理由で、OpenMP チューニングに使用するには不十分である。
スレッド・プロファイラは、各 OpenMP リージョン内で、個々のスレッドのレベルまで、
OpenMP スレッド化構文を理解し、アプリケーションの実行全体にわたる OpenMP のパ
フォーマンスを測定するように設計されている。スレッド・プロファイラは、負荷の不均衡
(各スレッドに割り当てられる計算量の不均衡)
、同期オブジェクトの待ち時間とクリティカ
ル・リージョンでの所要時間、バリアでの所要時間、インテル® OpenMP ランタイム・エン
ジンでの所要時間(並列オーバーヘッド)を検出し、測定できる。
23
マルチスレッド・アプリケーションの開発
推奨事項
スレッド・プロファイラ分析用の OpenMP アプリケーションを準備するために、OpenMP プ
ロファイリング・ライブラリを含む実行ファイルを作成する(/Qopenmp_profile コンパ
イラ・スイッチを使用する)
。VTune™ パフォーマンス・アナライザ内でスレッド・プロファ
イラのアクティビティを設定するときは、適切な数のスレッドで動作するフル実稼動データ
セットを必ず使用する。実稼動データのパフォーマンス・チューニングで最良の結果を得る
には、できるだけ通常に近い方法でコードをテストする、代表性のあるデータセットを使用
する必要がある。小さなテスト・データセットでは、コードの並列性やスレッド間の相互作
用を十分にテストできないため、重大なパフォーマンス上の問題を見逃す恐れがある。
OpenMP スレッドのインストルメーションの分だけ実行時間が長くなるが、この延長はごく
わずかである。
アプリケーションの実行が終了すると、スレッド・プロファイラ・ウィンドウにサマリ・パ
フォーマンス結果が表示される。パフォーマンス・データには 3 種類のグラフィック表示を
使用できる。各ビューは、Legend フレームの下にある別々のタブからアクセスできる。3 つ
のビューについて以下に説明する。
•
サマリビュー:スレッド・プロファイラは、デフォルトではこのビューを表示する
(図 3)。ヒストグラム・バーは、観察対象のパフォーマンス・カテゴリでのアプリ
ケーションの平均所要時間を示す多くのリージョンに分割される。観察対象のパ
フォーマンス・カテゴリには、次のものがある。
24
■
並列実行(OpenMP 並列リージョン内の時間)- 緑(1.)
■
逐次コードの所要時間 - 青(2.)
■
スレッド間の負荷の不均衡によるアイドル時間 - 赤(3.)
■
バリアでの待機中のアイドル時間 - 紫(4.)
■
同期オブジェクトへのアクセス待機中のアイドル時間 - オレンジ(5.)
■
クリティカル・リージョンでの実行時間 - グレー(6.)
■
並列オーバーヘッド(OpenMP ランタイム・エンジンでの所要時間)と逐
次オーバーヘッド(並列実行されない OpenMP リージョンでの所要時間)それぞれ黄色(7.)とオリーブ(8.)
インテル® ソフトウェア開発製品
2
マウスの左ボタンでバーをクリックすると、アプリケーションの実行全体にわたる各カテゴ
リの全実行時間の詳細な数値が、Legend フレームに表示される。
図 3.
スレッド・プロファイラのサマリビュー
3.
1.
1.
2.
3.
4.
5.
6.
7.
8.
もちろん、最良の表示は、大部分が緑(1.)で、青(2.)の逐次実行時間がわずかしかない
ヒストグラムである。サマリ・ヒストグラム内に他の色が大量に表示されている場合は、パ
フォーマンス上の問題を示している。問題の重大度は、表示された問題のタイプと、そのカ
テゴリでの実際の所要時間によって異なる。比較的小さなパフォーマンス上の問題は(特
に、アルゴリズムのコーディングの理由で、簡単な修復が不可能な場合は)許される範囲内
である。
また、サマリビューを使用して、さまざまな数のスレッドでアプリケーションのスケーラビ
リティを比較できる。さまざまな数のスレッドで実行される、同じデータを使用する同じ
コードの各種のアクティビティの実行を、サマリビューにドラッグ・アンド・ドロップす
る。スケーラビリティを示す以外に、スレッドの数を変化させると、一部のパフォーマンス
の障害個所が明らかになる。例えば、スレッドの数を増やすと、多くの場合はロックの競合
が増加する。これによって、一部のアプリケーションでは、十分なリソースが利用可能で
あっても、十分な高速化が妨げられる。
サマリビューに表示されたパフォーマンス上の問題を修正すると決めた後、問題の原因を特
定するために、より詳細な分析が必要になる。これを行うには、リージョン・ビューでタイ
ミング・データを検討する。
25
マルチスレッド・アプリケーションの開発
•
リージョン・ビュー:このビューは、ソースコード内の各リージョンについて、サマリ
データの内訳を調べる(図 4)。これらのリージョンには、OpenMP 並列リージョンとそ
の周囲の Serial リージョンが含まれる。リージョン・ビューによって、コードのどの部
分が(1 つのリージョンまたはすべてのリージョン)
、パフォーマンス上の問題の原因と
なっているかを確認できる。大きな Serial リージョンの観察によって、さらに並列化で
きるコードの部分を特定できる。リージョン・ヒストグラムをクリックすると、各パ
フォーマンス・カテゴリでの所要時間に関する詳細な数値が Legend フレームに表示さ
れる。Legend フレーム内で、複数のリージョンを選択し、比較できる。
図 4.
スレッド・プロファイラのリージョン・ビューと Legend
1.
図 4 は、アプリケーションから得られた一連の並列リージョンと Serial リージョンを示して
いる。このビューには、アプリケーションの大半の所要時間を使っている 1 つの並列リー
ジョン(A0R39)、複数の小さな並列リージョン、複数の Serial リージョンが含まれている。
表示されている Serial リージョンは小さすぎて、これ以上並列化することはできない。
選択されているリージョン・ヒストグラム(青い輪郭線(1.)で囲まれた部分)を右クリッ
クすると、ソースコード表示オプションのメニュー・ダイアログがポップアップされる。こ
れにより、チューニングするリージョンを決めたら、直ちに対応するソースコードを見付け
て、原因の検討と解決策の工夫ができる。各リージョンに対応するソースコードの位置も、
Legend フレームに表示される。
26
インテル® ソフトウェア開発製品
•
2
スレッドビュー:スレッドビューは、アプリケーションのタイミング特性をより詳細に
表示する(図 5)。実行に使用された各スレッドごとに、別々のヒストグラムが表示され
る。表示されるデータは、デフォルトでは、各スレッドのパフォーマンスを分析した、
実行全体のサマリデータである。逐次コードの所要時間を含むスレッドは、マスタス
レッドだけである。他のすべてのスレッド・ヒストグラムは、逐次コードの所要時間の
分だけ短くなる。
図 5.
スレッド・プロファイラのスレッドビュー
リージョン・ビューから分析を始めて、目的のリージョン以外のすべてのリージョ
ンを除外する。次に、スレッドビューを使用して、特定のリージョン内の個々のス
レッドのパフォーマンスに集中して分析できる。この詳細レベルでは、パフォーマ
ンス上の問題の原因について、より多くの手がかりが得られる。例えば、すべての
スレッドがほぼ同じ大きさのパフォーマンス・オーバーヘッドを示しているか、パ
フォーマンス・オーバーヘッドは特定の 1 スレッド内にのみ表れているか、あるい
は、他のパフォーマンスのパターンは存在するか、である。
図 5 では、スレッドビューは、
1 つの並列リージョンまで絞り込まれている。このリー
ジョン内で使用される 4 つのスレッドに、階段状の負荷の不均衡が表れている。こ
のパフォーマンス関係は、ループの反復ごとに計算量が増える規則的パターンを示
している。つまり、ループの次の反復には、その前の反復より長い時間がかかる。
OpenMP は、デフォルトでは静的スケジューリングを使用する。個々の反復の間で
の計算時間の増加率はほぼ一定であるから、小さなチャンクサイズを使用した静的
スケジューリングによって、正しいロード・バランスが得られ、パフォーマンス上
のボトルネックが解決されるはずである。ループの反復ごとの作業量の変化が予測
しにくい場合は、反復を動的にスケジューリングすることを推奨する。
27
マルチスレッド・アプリケーションの開発
使用ガイドライン
スレッド・プロファイラは、現在のところ、Microsoft Windows オペレーティング・システ
ム上で実行される、OpenMP でスレッド化されたコードと Win32 API のクリティカル・セク
ションをサポートしている。OpenMP でスレッド化されるプログラムをコンパイルし、
OpenMP プロファイリング・ライブラリを利用可能にするには、インテル® 7.0 コンパイラ以
降が必要である。
参考資料
本シリーズの参照個所 :
本章、2.3「VTune™ パフォーマンス・アナライザによるスレッド間のフォル
ス・シェアリングの回避と特定」
アプリケーションのスレッド化、3.2「粒度と並列性能」
アプリケーションのスレッド化、3.3「ロード・バランスと並列性能」
アプリケーションのスレッド化、3.8「順序付けされたデータ・ストリーム内
のデータ並列性の利用」
アプリケーションのスレッド化、3.9「ループ・パラメータの操作による
OpenMP パフォーマンスの最適化」
同期、4.1「ロックの競合と大小のクリティカル・セクションの管理」
メモリ管理、5.2「スレッドごとのローカル・ストレージによる同期の削減」
他の参考資料 :
スレッド・プロファイラの Web サイト
『Getting Started with the Intel Threading Tools』(インテル・スレッド化ツールに
同梱)
インテル・ソフトウェア・カレッジでは、インテル・ソフトウェア開発製品に
関する広範囲にわたる研修教材を提供している。本項目については、オンライ
ン・コース「インテル・スレッド化ツールの使用」の受講をお勧めする。
28
3
アプリケーションのスレッド化
本章は、アプリケーションのスレッド化に関する一般的な事項(特に並列性能に関するも
の)を対象とする。必要に応じて API 固有の問題に触れるが、ほとんどの推奨事項は、すべ
ての並列プログラミング手法に適用される。
本章では、まず始めに、データ並列性と機能的分解の対比について説明する。最初の項目で
は、2 種類の並列モデルから最適なスレッド化手法を選択するための推奨事項を示す。続く
2 つの項目では、粒度と負荷バランスについて説明する。粒度と負荷バランスは、マルチス
レッド・アプリケーションの効率性とスケーラビリティに直接影響を与えるため、並列プロ
グラミングでは重大な問題になる。
マルチスレッド・プログラムにおいて、特定のランタイム環境に合わせてスレッドの動作を
調整することは、多くの場合見過ごされている。例えば、シングルユーザ・システムでは、
アイドル状態のスレッドは、スリープさせるよりスピンさせる方が効率的である。しかし、
共有システムでは、アイドル状態のスレッドに CPU を解放させる方が効率的になる場合が
ある。この項目では、ターンアラウンド重視のスレッド化とスループット重視のスレッド化
に関する問題について説明する。
多くのアルゴリズムに含まれる最適化手法は、逐次性能を向上させるが、意図しない依存関
係を発生させ、並列実行の妨げになる可能性がある。多くの場合、簡単な変換によって、こ
のような依存関係を解決できる。この項目では、見かけの依存関係を回避または削除して、
並列性を顕在化させる手法について説明する。
次の 2 つの項目では、適切なスレッド数の選択方法と、スレッド作成のオーバーヘッドを最
小限に抑える方法について説明する。必要以上に多くのスレッドを作成すると、システム・
オーバーヘッドが大きくなる、粒度が細かくなる、ロックの競合が増えるなど、多くの理由
でパフォーマンスに悪影響を与える。従って、実行時の発見的手法とスレッドプールを利用
して、スレッド数を制御することを推奨する。発見的手法によって、プログラマは、実行時
までわからないワークロードの必要条件に基づいて、スレッドを作成できる。この項目で
は、スレッドプールを使用してスレッド作成のオーバーヘッドを制限する方法についても説
明する。この項目の推奨事項は、主に Pthreads またはスレッド関連の Win32* API でスレッ
ド化されたアプリケーションに適用される。インテル® OpenMP* ライブラリには、スレッド
プールがすでに使用されている。
本章では、最後に、順序依存出力の処理方法と、OpenMP パフォーマンスの向上を目的とす
るループ最適化手法について説明する。
29
マルチスレッド・アプリケーションの開発
3.1 適切なスレッド化手法の選択 : OpenMP* と明示的スレッド化
カテゴリ
アプリケーションのスレッド化
記述範囲
一般的なマルチスレッド処理
キーワード
OpenMP*、POSIX スレッド、Pthreads、Win32* スレッド、データ並列性、機能的分解
摘要
最も一般的なマルチスレッド化手法は、コンパイラ方式とライブラリ方式の 2 種類である
が、いずれもすべての状況に適合するとは言えない。OpenMP などのコンパイラ方式のス
レッド化手法は、データ並列性に最適である。スレッド化ライブラリ(特にスレッド関連の
Win32 API と POSIX* API)に基づく手法は、機能的分解に最適である。
背景情報
プログラマは、長年にわたって、スレッドを使用してアプリケーションを無理なく並行に実
行できるようにしてきた。例えば、スレッドによって、アプリケーションは処理を続けなが
ら GUI 入力を受け入れられる。従って、ユーザ側から見てアプリケーションが反応しなく
なることはない。対称型マルチプロセッサまたはハイパー・スレッディング・テクノロジ対
応 CPU 上では、スレッドを使用した並列コンピューティングによって、パフォーマンスが
大きく向上する。
大まかに言って、ライブラリ方式とコンパイラ方式の 2 種類のスレッド化手法が有効であ
る。それぞれの手法は、最適なマルチスレッド・プログラミングのタイプが異なる。ライブ
ラリ方式のスレッド化手法(Windows* ではマルチスレッド関連の Win32 API、Linux* では
Pthreads ライブラリ)では、プログラマが手作業で並行実行タスクをスレッドに対応付ける
必要がある。スレッド間に明示的な親子関係はなく、すべてのスレッドが対等である。この
ため、ライブラリ方式のスレッド化モデルは、汎用性が非常に高い。また、マルチスレッド
関連のライブラリによって、プログラマは、スレッドの作成、管理、同期について低レベル
まで制御できる。このような柔軟性は、ライブラリ方式のスレッド化手法の主要な利点であ
るが、これにはコストがかかる。既存の逐次アプリケーションをライブラリ方式の手法でス
レッド化すると、大量のコード修正が必要になる。並行実行タスクは、スレッドに対応付け
られる関数の中にカプセル化しなければならない。また、POSIX スレッドと Win32 スレッ
ドは、引数を 1 つしか受け入れない。従って、多くの場合、関数プロトタイプとデータ構造
の変更が必要になる。
30
アプリケーションのスレッド化
3
OpenMP(コンパイラ方式のスレッド化手法)は、基礎的なスレッド・ライブラリに対する
高レベルのインターフェイスを提供する。OpenMP では、プログラマは、プラグマ(Fortran
の場合はディレクティブ)を使用して、コンパイラに対して並列性を記述する。これによっ
て、コンパイラが詳細な部分を自動的に処理するため、明示的スレッド化の複雑な作業の大
部分が不要になる。また、OpenMP では、通常は大量のソースコードの修正は不要であるた
め、プログラミングの手間が軽減される。OpenMP 非対応コンパイラは、単に OpenMP プラ
グマを無視し、基礎となる逐次コードをそのまま残す。
ただし、OpenMP は、スレッドを細かく制御する機能を持っていない。特に、OpenMP には、
プログラマがスレッドの優先順位を設定したり、イベントに基づく同期やプロセス間の同期
を実行する方法がない。その上、OpenMP は、スレッド間の明示的なマスター / ワーカー関
係に基づく fork-join スレッド化モデルである。
このため、OpenMP が適合する問題の範囲は限定される。
一般的なワード・プロセッサには、多くの局面で並行実行性が利用されている。ユーザが
キーボードから入力している間、キーボード入力を中断せずに、複数のバックグラウンド・
タスクが同時に実行される。このアプリケーションは、例えば、変更内容の定期的な保存、
スペルと文法のチェック、文書の印刷を実行する。これが機能的分解の例である。機能的分
解では、並行実行を目的として異なるタスクが各スレッドに対応付けられ、並行実行性の度
合はタスクの数によって決まる。ライブラリ方式のスレッド化手法は、高い汎用性と細かい
制御が得られるため、このタイプの並行実行性を表現するのに適している。例えば、キー
ボード入力を処理するスレッドに、印刷などの重要度の低いタスクを処理するスレッドより
高い優先順位を指定できる。
OpenMP は、データ並列性を表現するために設計された手法である。データ並列性とは、各
スレッドが異なるデータに対して同じタスクを実行することである。データ並列アプリケー
ションの例は、Web サーバである。Web サーバは、異なるデータ(Web ページ)に対して同
じタスク(HTTP 要求へのサービス)を繰り返し実行する。データ並列性問題では、並列性
の度合はデータの量によって決まる。ワード・プロセッサのスペル・チェッカーが良い例で
ある。文書中の単語がスレッド間で配分され、各スレッドが独立して単語の照合を実行す
る。文書中の単語の数が増えるにつれて、並列実行される仕事の量が増加する。
31
マルチスレッド・アプリケーションの開発
推奨事項
一般的に、データ並列性を表現するには、OpenMP が最適である。一方、機能的分解には、
明示的スレッド化手法(すなわち、Pthreads ライブラリとスレッド関連の Win32 API)が最
適である。以下の例に示すように、データ並列性問題には明示的スレッド化手法を適用しな
いこと。以下のプログラムは、数値積分計算を実行する。この並列性は、1 つの OpenMP プ
ラグマで表現できる(すでに述べたように、OpenMP 非対応コンパイラは、単にこのプラグ
マを無視し、基礎となる逐次コードをそのまま残す)。
#include <stdio.h>
#define INTERVALS 100000
int main ()
{
int i;
float h, x, pi = 0.0;
h = 1.0 / INTERVALS;
#pragma omp parallel for private(x) reduction(+:pi)
for (i = 0; i < INTERVALS; i++)
{
x = h * (float(i) ? 0.5);
pi += 4.0 / (1.0 + x * x);
}
pi *= h;
printf (“Pi = %f\n”, pi);
}
32
アプリケーションのスレッド化
3
次のように、Pthreads やスレッド関連の Win32 API などの明示的スレッド化手法でデータ並
列性を表現することもできるが、この方法は非効率である。
#include <stdio.h>
#include <pthreads.h>
#define INTERVALS 100000
#define THREADS 4
float global_sum = 0.0;
pthread_mutex_t global_lock = PTHREAD_MUTEX_INITIALIZER;
void *pi_calc (void *num);
int main ()
{
pthread_t tid[THREADS];
int i, t_num[THREADS];
for (i = 0; i < THREADS; i++)
{
t_num[i] = i;
pthread_create (&tid[i], NULL, pi_calc, &t_num[i]);
}
for (i = 0; i < THREADS; i++)
pthread_join (tid[i], NULL);
printf (“Pi = %f\n”, global_sum);
}
void *pi_calc (void *num)
{
int i, myid, start, end;
float h, x, my_sum = 0.0;
myid = *(int *)num;
h = 1.0 / INTERVALS;
start = (INTERVALS / THREADS) * myid;
end = start + (INTERVALS / THREADS);
for (i = start; i < end; i++)
{
x = h * ((float)i - 0.5);
my_sum += 4.0 / (1.0 + x * x);
}
pthread_mutex_lock (&global_lock);
global_sum += my_sum;
pthread_mutex_unlock (&global_lock);
}
このコードでは、プログラムのサイズと複雑性が大幅に増し、元の逐次コードの見分けがつ
かなくなっている。この計算は、スレッドに対応付けられる関数の中にカプセル化されてい
る。この関数の中で、計算がスレッド間に手作業で配分されている。
明示的スレッド化は、機能的分解を表現するために設計された手法である。機能的分解で
は、仕事はデータではなくタスクに基づいて分割される。明示的スレッド化手法では、プロ
グラマが並行実行タスクをスレッドに対応付ける。コンカレント・プログラミングの教科書
に記載されている、標準的な生産者 / 消費者(producer-consumer)問題の場合を考える。
33
マルチスレッド・アプリケーションの開発
明示的スレッド化 API を使えば、プログラマがスレッドを動的に作成し削除できるため、生
産者 / 消費者モデルを簡単にコーディングできる。さらに、同期がデータアクセスだけに限
られない。スレッドにイベントを待機させて、イベントに基づく同期が可能である。OpenMP
は、イベントに基づく同期機能を持たないため、このような簡単な問題でも効率的なコー
ディングは難しい。OpenMP の sections プラグマは、機能的分解をコーディングする機能
を多少持っているが、基本的に fork-join スレッド化モデルであるため、柔軟性とスケーラビ
リティが制限される。厳密には、並列セクションの数はコンパイル時に固定されるため、プ
ロセッサのリソースの変化に応じて、実行時に生産者スレッドと消費者スレッドの数を動的
に変更することはできない。また、OpenMP には、スレッドに優先順位を割り当てる機能が
ない。
使用ガイドライン
OpenMP、Pthreads、Win32 スレッドの間でスレッド化手法を選択する際は、移植性も考慮に
入れる必要がある。OpenMP 対応コンパイラは、Windows および Linux などのほとんどのオ
ペレーティング・システムで利用できる。一方、スレッド・ライブラリには移植性がない。
明らかに、Win32 API は、Microsoft のオペレーティング・システム上でしか利用できない。
さらに、Windows の異なるバージョンは、サポートしている機能に多少違いがある。Linux
およびその他の UNIX* の変種上で使用される Pthreads も、同じような制限を受ける。
並列性能が目的でアプリケーションをスレッド化する場合は、スケーラビリティを考慮に入
れる必要がある。並列性の度合を決めるのは、互いに独立したタスクの数か、処理される
データの量か、あるいはその両方か。大量の計算を必要とする、互いに独立した 2 つのタス
クだけを実行するアプリケーションの場合を考える。例えば、4 個の CPU を搭載したマル
チプロセッサ・システム上で、これらのタスクを Win32 スレッドまたは POSIX スレッドに
対応付けると、システムの半分しか使用されない。2 つのタスクがデータ並列性を持つ場合
は、各タスクに OpenMP を追加することを推奨する。しかし、1 つのタスクがデータ並列性
を持ち、もう 1 つのタスクがデータ並列性を持たない場合は、OpenMP だけではシステムを
フルに利用できない(アムダールの法則については、並列計算に関する教科書を参照)。こ
の例では、互いに独立したタスクを両方とも Win32 スレッドまたは POSIX スレッドに対応
付けてから、OpenMP を使用して各タスク内のデータ並列性を表現することを推奨する。
34
アプリケーションのスレッド化
3
参考資料
本章の参照個所 :
3.2「粒度と並列性能」
3.3「ロード・バランスと並列性能」
3.6「ワークロードの発見的手法による実行時の適切なスレッド数の決定」
3.8「順序付けされたデータ・ストリーム内のデータ並列性の利用」
他の参考資料 :
『OpenMP C and C++ Application Program Interface』(バージョン 2.0)、OpenMP
Architecture Review Board、2002 年 3 月
『OpenMP Fortran Application Program Interface』(バージョン 2.0)、OpenMP
Architecture Review Board、2000 年 11 月
『Multithreading: Taking Advantage of Intel Architecture-based Multiprocessor
Workstations』、インテル・ホワイトペーパー、1999 年
『Performance improvements on Intel architecture-based multiprocessor workstations:
Multithreaded applications using OpenMP』
、インテル・ホワイトペーパー、2000 年
『Threading Methodology: Principles and Practices』
、インテル技術レポート、2002 年
M. Ben-Ari 著『Principles of Concurrent Programming』、Prentice-Hall International
刊、1982 年
David R. Butenhof 著『Programming with POSIX Threads』、Addison-Wesley 刊、
1997 年
Johnson M. Hart 著『Win32 System Programming(2nd Edition)』、Addison-Wesley
刊、2001 年
Jim Beveridge、Robert Wiener 共著『Multithreading Applications in Win32』、
Addison-Wesley 刊、1997 年
35
マルチスレッド・アプリケーションの開発
3.2 粒度と並列性能
カテゴリ
アプリケーションのスレッド化
記述範囲
一般的なマルチスレッド処理とパフォーマンス
キーワード
粒度、ロード・バランス、並列オーバーヘッド、VTune™ アナライザ、スレッド・プロファイラ
摘要
高い並列性能を得るための鍵は、アプリケーションに合った粒度を選択することである。粒
度とは、1 つの並列タスク内の仕事の量である。粒度が細かすぎると、通信オーバーヘッド
のためにパフォーマンスが低下する。粒度が粗すぎると、負荷の不均衡のためにパフォーマ
ンスが低下する。目標は、並列タスクの適切な粒度を決定して(通常は粗い粒度の方が良
い)、負荷の不均衡と通信オーバーヘッドを回避し、最適なパフォーマンスを得ることであ
る。
背景情報
マルチスレッド・アプリケーションの並列タスク当たりの仕事の量(すなわち、粒度)は、
アプリケーションの並列性能に大きな影響を与える。アプリケーションをスレッド化する場
合、最初の手順は、問題をできるだけ多くの並列タスクに分割することである。2 番目の手
順では、データと同期のために必要な通信を決定する。3 番目の手順では、アルゴリズムの
パフォーマンスを検討する。通信操作と分割操作にはコストがかかるため、開発者は多くの
場合、集積(複数のパーティションの合成)によってオーバーヘッドを克服し、効率的な
コードを作成する必要がある。集積の手順は、アプリケーションに最適な粒度を決定するプ
ロセスである。
粒度は、多くの場合、スレッド間のワークロードのバランスのとり方に関連している。多数
の小さいタスクに分割する方が、ワークロードのバランスはとりやすいが、タスクの数が多
すぎると、並列オーバーヘッドが大きくなる。従って、通常は粗い粒度の方が良い。しか
し、必要以上に粒度を大きくすると、負荷の不均衡が発生する(3.3「ロード・バランスと
並列性能」を参照)。インテル ® スレッド・プロファイラ(2.5「スレッド・プロファイラに
よる OpenMP* パフォーマンスの評価」を参照)などのツールを使用して、アプリケーショ
ンに合った粒度を簡単に見つけられる。
36
アプリケーションのスレッド化
3
以下の例は、同期オーバーヘッドの削減とスレッドの最適な粒度の発見によって、並列プロ
グラムのパフォーマンスを向上させる方法を示している。この項目全体を通して使用される
例は、素数生成問題(すなわち、0 ~ 100 万の範囲内のすべての素数の発見)である。コー
ド例 1 は、OpenMP* を使用した並列コードを示している。
コード例 1. OpenMP によって並列化された素数生成コード
#pragma omp parallel for \
schedule(dynamic, 1) \
private(j, limit, prime)
for (i = start; i <= end; i += 2) // Between 0 and 1
million
{
limit = (int) sqrt((float)i) + 1;
prime = 1; // Assume number is prime
j = 3;
while (prime && (j <= limit))
{
if (i%j == 0) prime = 0;
j += 2;
}
if (prime)
{
#pragma omp critical
{
number_of_primes++;
if (i%4 == 1) number_of_41primes++; // 4n+1
primes
if (i%4 == 3) number_of_43primes++; // 4n-1
primes
}
}
}
37
マルチスレッド・アプリケーションの開発
このコードは、同期のための通信オーバーヘッドが大きく、ワークロードが小さすぎるた
め、スレッド化のメリットを活かせていない。まず、ループ内のクリティカル・セクション
が、カウント変数をインクリメントするための安全機構を提供していることがわかる。図 6a
のインテル・スレッド・プロファイラ表示に示すように、このクリティカル・セクションに
よって、並列ループの同期とロックのオーバーヘッドが増加している。
図 6.
VTune™ アナライザのスレッド・プロファイラ表示
a)A0: 1 回目の実行、同期とロックのオーバーヘッド、b)A1: 2 回目の実行、並列オーバー
ヘッド、c)A2: 3 回目の実行、負荷の不均衡、d)A3: 4 回目の実行、パフォーマンス上の問題
の解決
38
アプリケーションのスレッド化
3
ロックと同期のオーバーヘッドは、クリティカル・セクションを OpenMP の reduction 節
で置き換えれば解決される(コード例 2 を参照)。カウンタ変数のインクリメントは、リダ
クションと呼ばれる一般的な操作である。OpenMP の reduction 節は、リダクション操作
を処理する効率的な方法を提供する。
コード例 2. critical プラグマの代わりに reduction 節を使用して OpenMP によって並列
化された素数生成コード
#pragma omp parallel for \
schedule(dynamic, 1) private(j, limit,
prime) \
reduction(+: number_of_primes, \
number_of_41primes, \
number_of_43primes)
for (i = start; i <= end; i += 2) // Between 0 and 1
million
{
limit = (int) sqrt((float)i) + 1;
prime = 1; // Assume number is prime
j = 3;
while (prime && (j <= limit))
{
if (i%j == 0) prime = 0;
j += 2;
}
if (prime)
{
number_of_primes++;
if (i%4 == 1) number_of_41primes++; // 4n+1
primes
if (i%4 == 3) number_of_43primes++; // 4n-1
primes
}
}
インテル・スレッド・プロファイラは、ロックとの同期オーバーヘッドは解決されたが、並
列オーバーヘッドが存在することを示している(図 6b)。動的スケジューリングによって、
若干のオーバーヘッドが発生する。schedule(dynamic, 1) 節は、各スレッドに一度に 1
つの反復(すなわち、チャンクサイズ)を動的に配分するように、スケジューラに指示す
る。各スレッドは、1 つのループ反復を処理した後、スケジューラに戻って次の反復を取得
する。schedule 節でチャンクサイズを大きくすると、スレッドがスケジューラに戻る回数
が減少する。
しかし、チャンクサイズが大きすぎると、負荷の不均衡が発生する。例えば、チャンクサイ
ズを 100,000 まで大きくすると、インテル・スレッド・プロファイラは負荷の不均衡を示す
(図 6c)。負荷の不均衡が発生するのは、反復 900,000 ~ 1,000,000 に含まれる仕事の量が、
それ以前のチャンクより大きいからである。チャンクサイズを 100 に設定すると、並列オー
バーヘッドと負荷の不均衡が解消される(図 6d)。
39
マルチスレッド・アプリケーションの開発
推奨事項
マルチスレッド・アプリケーションの並列性能は、粒度(すなわち、並列タスク当たりの仕
事の量)によって変化する。一般的に、スレッド間の負荷の不均衡が生じない範囲で、でき
るだけ粗い粒度にすることを推奨する。スレッド当たりの仕事の量が、スレッド化のオー
バーヘッドよりはるかに大きくなるように注意する。インテル・スレッド・プロファイラを
使用して、大きすぎる並列オーバーヘッド、必要以上の同期、負荷の不均衡を検出できる。
使用ガイドライン
上記の説明は主に OpenMP を対象としているが、この節で説明したすべての推奨事項と原則
は、Win32 スレッドや POSIX スレッドなどのスレッド化手法にも適用される。
参考資料
本シリーズの参照個所 :
インテル® ソフトウェア開発ツール、2.5「スレッド・プロファイラによる
OpenMP パフォーマンスの評価」
本章、3.1「適切なスレッド化手法の選択 : OpenMP と明示的スレッド化」
本章、3.3「ロード・バランスと並列性能」
本章、3.6「ワークロードの発見的手法による実行時の適切なスレッド数の決定」
同期、4.1「ロックの競合と大小のクリティカル・セクションの管理」
他の参考資料 :
Rohit Chandra 他著『Parallel Programming in OpenMP』、Morgan Kaufman 刊、
2001 年
Ian T. Foster 著『Designing and Building Parallel Programs: Concepts and Tools for
Parallel Software Engineers』
、Addision-Wesley 刊、1995 年
Ding-Kai Chen 他著『The Impact of Synchronization and Granularity on Parallel
Systems』
、Proceedings of the 17th Annual International Symposium on Computer
Architecture、1990 年
40
アプリケーションのスレッド化
3
3.3 ロード・バランスと並列性能
カテゴリ
アプリケーションのスレッド化
記述範囲
一般的なマルチスレッド処理
キーワード
粒度、ロード・バランス、スレッドのスケジューリング、VTune™ アナライザ、スレッド・
プロファイラ
摘要
スレッド間のアプリケーションのワークロードのロード・バランスは、アプリケーションの
パフォーマンスにとってきわめて重要である。ロード・バランスの主な目的は、スレッドの
アイドル時間を最小限に抑えることである。ワークロードがすべてのスレッドに均等に分散
され、ワークシェアリングのオーバーヘッドが最小限に抑えられれば、実行のクリティカ
ル・パスが最短になるため、最適なパフォーマンスが得られる。しかし、完璧なロード・バ
ランスを達成するのは簡単ではない。また、どの程度ロード・バランスがとれるかは、アプ
リケーション内の並列性、ワークロード、スレッド数、ロード・バランス・ポリシー、ス
レッド化コードによって決まる。
背景情報
計算中にプロセッサがアイドル状態になると、リソースの浪費になり、計算の実行時間が長
くなる。このアイドル状態は、メモリからのフェッチや I/O などの多くの原因によって発生
する。プロセッサがときどきアイドル状態になることは避けられないが、プログラマがアイ
ドル時間を短縮する手段はいくつか存在する(例えば、オーバーラップ I/O、メモリ・プリ
フェッチ、データ・アクセス・パターンの変更によるキャッシュ利用率の向上など)
。
同様に、アイドル状態のスレッドも、マルチスレッド実行時のリソースの浪費になる。各ス
レッドに割り当てられる仕事の量が均等でない状態は、負荷の不均衡と呼ばれる。負荷の不
均衡が大きいほど、多くのスレッドがアイドルのままになり、計算の完了までの時間が長く
なる。利用可能なスレッドに対する計算タスクの配分が均等であるほど、実行時間は全体と
して短縮される。
41
マルチスレッド・アプリケーションの開発
例として、互いに独立した 12 個のタスクを実行する場合を考える。各タスクの所要時間は、
{10, 6, 4, 4, 2, 2, 2, 2, 1, 1, 1, 1} である。この一連のタスクの計算に 4 つのスレッドを利用で
きるとすると、タスク割り当ての簡単な方法は、各スレッドに 3 つのタスクを順番に配分し
てスケジューリングすることである。従って、スレッド 1 には合計 20 時間単位(10+6+4)
の仕事が割り当てられ、スレッド 2 には 8 時間単位(4+2+2)、スレッド 3 には 5 時間単位
(2+2+1)が必要になるが、スレッド 4 は割り当てられたタスクを 3 時間単位(1+1+1)で実
行できる。図 7 は、この仕事の配分と、これらの 12 個のタスクの実行時間が全体として 20
時間単位になることを示している。
図 7.
負荷の不均衡を示すタスク配分の例
スレッド 1
42
スレッド 2
スレッド 3
スレッド 4
アプリケーションのスレッド化
3
仕事の配分を改善するには、スレッド 1 {10}、スレッド 2 {6, 1, 1}、スレッド 3 {4, 2, 1, 1}、
スレッド 4 {4, 2, 2, 2} の割り当てにする。このスケジュールなら、計算は 10 時間単位で完
了する。4 スレッドのうち 2 スレッドだけが、それぞれ 2 時間単位アイドルになる(図 8)。
図 8.
ロード・バランスが改善されたタスク配分の例
スレッド 1
スレッド 2
スレッド 3
スレッド 4
推奨事項
すべてのタスクが同じ長さであれば、単に利用可能なスレッド間でタスクを静的に分割する
(タスクの総数を(ほぼ)等しいサイズのグループに分割し、それを各スレッドに割り当て
る)のがベストである。しかし、一般的に、すべてのタスクの長さが前もってわかっていて
も、スレッドに対するタスクのバランスのとれた最適な割り当てを見付けることは、難しい
問題である。
個々のタスクの長さが同じでない場合は、スレッドにタスクを動的に割り当てる方が良い。
OpenMP* は、反復ワークシェアリング構文の 4 つのスケジューリング手法を備えている(各
手法の詳細は、OpenMP の仕様を参照)。デフォルトでは、反復の静的スケジューリングが
使用される。反復ごとの仕事の量が変化し、変化のパターンが予測できない場合は、反復の
動的スケジューリングの方がワークロードのバランスが向上する。OpenMP では、動的スケ
ジューリングには dynamic と guided の 2 つのオプションがあり、schedule 節で指定さ
れる。dynamic スケジューリングでは、反復のチャンクがスレッドに割り当てられる。こ
の割り当てが完了すると、各スレッドは新しい反復のチャンクを要求する。schedule 節の
オプションのチャンク引数は、dynamic スケジューリングで割り当てられる反復の固定数を
指定する。guided スケジューリングでは、各スレッドに割り当てられる反復のチャンクサイ
ズが段階的に小さくなる。この割り当てパターンによって、guided スケジューリングに必
要なオーバーヘッドは dynamic スケジューリングより小さくなる。schedule 節のオプショ
ンのチャンク引数は、guided スケジューリングで割り当てられる反復の最小数を指定する。
43
マルチスレッド・アプリケーションの開発
反復ごとの仕事の量が単調に増加(または減少)する特殊な場合もある。例えば、下三角行
列の行ごとの要素数は、規則的に増加する。このような場合は、静的スケジューリングに
よってチャンクサイズを設定すれば、dynamic スケジューリングや guided スケジューリング
によるオーバーヘッドの増加なしに、満足のいくロード・バランスが得られる。
スケジューリング手法の選び方がわからない場合は、runtime スケジュールを使用して、
スケジューリング手法とチャンクサイズを実行時に指定する。この方法では、プログラムを
再コンパイルせずに実験ができる。
明示的スレッド化手法(例えば、Win32 スレッドと POSIX スレッド)は、互いに独立した
一連のタスクをスレッドに対して自動的にスケジューリングする手段を持っていない。この
ような機能は、必要に応じてアプリケーションの中にプログラミングしなければならない。
タスクの静的スケジューリングは簡単な方法である。動的スケジューリングについては、生
産者 / 消費者(Producer/Consumer)モデルとマネージャ / ワーカー(Manager/Worker)モデ
ルと呼ばれる、関連する 2 つの手法を簡単にコーディングできる。生産者 / 消費者モデルで
は、1 つ以上のスレッド(生産者)がタスクをキューに入れ、必要に応じて消費者スレッド
がタスクを取り出して処理する。タスクが消費者スレッドに利用可能になる前に、何らかの
前処理が必要な場合は、
(必須ではないが)多くの場合は生産者 / 消費者モデルが使用され
る。マネージャ / ワーカモデルでは、必要な仕事が増えた場合、複数のワーカスレッドが 1
つのマネージャスレッドとランデブーして、直接割り当てを受ける。
どのモデルでも、要求された計算を実行するスレッドがアイドルのままにならないように、
スレッドの数と組み合わせに注意する必要がある。単一のマネージャ・スレッドは、簡単に
コーディングでき、タスクの適切な配分を保証する。一方、消費者スレッドが頻繁にアイド
ル状態になる場合は、消費者の数を減らすか、生産者スレッドの数を増やす必要がある。適
切な解決策は、アルゴリズムの必要条件と、割り当てられるタスクの数と長さによって異な
る。
使用ガイドライン
動的スケジューリングは、タスクの配分によって若干のオーバーヘッドが発生する。互いに
独立した小さいタスクを、1 つの割り当て可能な仕事のユニットにまとめれば、このオー
バーヘッドを軽減できる。
1 つのタスクを構成する計算量の最適な選択は、実行される計算と、実行時に利用可能なス
レッド数および他のリソースによって決まる(3.2「粒度と並列性能」を参照)
。
上記の説明は主に OpenMP を対象としているが、この節で説明したすべての推奨事項と原則
は、Win32 スレッドや POSIX スレッドなどのスレッド化手法にも適用される。
44
アプリケーションのスレッド化
3
参考資料
本シリーズの参照個所 :
インテル® ソフトウェア開発製品、2.5「スレッド・プロファイラによる
OpenMP パフォーマンスの評価」
本章、3.1「適切なスレッド化手法の選択 : OpenMP と明示的スレッド化」
本章、3.2「粒度と並列性能」
本章、3.6「ワークロードの発見的手法による実行時の適切なスレッド数の決定」
本章、3.9「ループ・パラメータの操作による OpenMP パフォーマンスの最適化」
他の参考資料 :
M. Ben-Ari 著『Principles of Concurrent Programming』、Prentice-Hall International,
Inc. 刊、1982 年
Ian Foster 著『Designing and Building Parallel Programs』
、Addison-Wesley 刊、
1995 年
Steven Brawer 著『Introduction to Parallel Programming』、Academic Press, Inc. 刊、
1989 年
45
マルチスレッド・アプリケーションの開発
3.4 ターンアラウンド重視のスレッド化とスループット重視の
スレッド化
カテゴリ
アプリケーションのスレッド化
記述範囲
一般的なマルチスレッド処理
キーワード
spin-wait、OpenMP*、Pthreads、Win32* スレッド、アイドル・ポリシー
摘要
イベントを待機している間にスレッドが何をしているかによって、アプリケーションの実行
速度に大きな差が出る。また、システム上の他のジョブとの関係にも注意する必要がある。
この問題は、システムの応答性と処理速度に大きな影響を与える。アプリケーションの使用
モデルを理解した上で、アプリケーションのターンアラウンド時間を重視して最適化する
か、システム・スループットを全体として満足のいくレベルに保つことを重視するかを選択
できる。
背景情報
コンピュータの使用目的は、専用計算エンジンと専用スループット・エンジンの 2 つの大き
なカテゴリに分類できる。専用計算エンジンの目的は、計算を実行している 1 つのジョブの
結果をできるだけ迅速に求めることである。専用スループット・エンジンの目的は、実行中
のすべてのジョブを満足のいく速度で進行させることである。例えば、天気予報を実行する
コンピュータは専用計算エンジンになる傾向があり、Web サーバを実行するコンピュータは
スループット・エンジンになる傾向がある。インタラクティブ・ワークステーションは、両
者の中間である。「バックグラウンド」アプリケーションの動作はスループット・エンジン
に似ており、
「フォアグラウンド」アプリケーションの動作は専用計算エンジンに似ている。
マルチスレッド・アプリケーションを設計する場合、アプリケーションを実行するユーザ
が、高いターンアラウンドを求めているのか、高いスループットを求めているのか、その両
方を要求しているのかを理解することが非常に重要である。使用目的を理解した上で、特定
の条件、複数の条件の切り替え、その両方での満足のいく性能のうちどれかを重視したアプ
リケーションを設計できる。
46
アプリケーションのスレッド化
3
マルチスレッド・プログラム内の各スレッドは、共有リソースを介したデータ交換によって
互いに通信する。Pthreads は、この目的のために、状態変数、セマフォ、mutex を用意して
いる。一方、スレッド関連の Win32 API は、イベント、セマフォ、mutex、クリティカル・
セクションと呼ばれる特殊な形式の mutex 変数を用意している。プログラマがこのようなリ
ソースを作成することもできる。この場合は、協調動作するスレッド間の通信のために特定
のメモリ上の位置をフラグとして使用し、何らかの volatile 型セマンティクスまたはアクワ
イア / リリース・セマンティクスを使ってその位置に注意深く書き込む。基礎となる手法に
関係なく、あるスレッドが共有リソースを獲得しようとしたとき、他のスレッドがそのリ
ソースを(排他状態で)既に保持していた場合は、リソースを獲得しようとするスレッドは
待機しなければならない。待機中のスレッドが何をしているかによって、アプリケーション
とシステム全体のパフォーマンスは大きな影響を受ける。待機中のスレッドの動作には、
spin-wait とブロッキングの 2 つの極端な状態がある。
spin-wait 状態のスレッドは、プロセッサをビジー状態にして、リソースが解放されたかどう
かを繰り返しチェックする。ブロッキング状態のスレッドは、ただちに CPU を解放してオ
ペレーティング・システムに渡し、リソースが利用可能になった時点でスレッドをウェーク
アップするように依頼する。最近のプログラムは、spin-wait とブロッキングの中間の状態を
用意しており、他のジョブの進行を妨げないように、spin-wait からブロッキングへのアダプ
ティブ・スイッチングが可能である。
待機中は、関数によって異なる種類の動作を実行する。例えば、以前の Linux Pthreads の wait
関数は待機中に spin-wait するが、Win32 の WaitForSingleObject および
WaitForMultipleObjects 関数はブロッキングする。Win32 の EnterCriticalSection
関数は、ユーザが制御可能な時間だけ spin-wait した後、関連するカーネル・オブジェクト上
でブロッキングする。OpenMP API は、実行時間のほとんどが計算処理となる(compute-bound)
同期アプリケーションの作成に最適である。このようなアプリケーションは、通常は 1 プロ
セッサ当たり 1 スレッドを割り当てる。従って、OpenMP の critical 構文および ordered
構文とロック API は、通常は spin-wait を実行する。インテル® OpenMP ライブラリは、スレッ
ドがブロッキングに入る前に spin-wait する時間の調整用のコントロールを提供する。
推奨事項
スレッドがリソースを保持する時間が非常に短い(例えば、数百クロックサイクル)場合
は、通常は spin-wait を使用する方がよい。これは、CPU を解放してオペレーティング・シ
ステムに渡すためのオーバーヘッドが、リソースを保持している時間より大きくなるためで
ある(同期、4.1「ロックの競合と大小のクリティカル・セクションの管理」を参照)。Windows
のクリティカル・セクション関数(同期、4.3「同期用の Win32 アトミック、ユーザ空間ロッ
ク、カーネル・オブジェクトの対比」を参照)および OpenMP の critical 構文とロック API
は、この目的に効果的である。
47
マルチスレッド・アプリケーションの開発
同時にアクティブになるスレッド数がプロセッサ数を超えない専用システム上で実行され
る、実行時間のほとんどが計算処理となるアプリケーションでは、少なくとも短時間
spin-wait する API を使用する方が、通常はアプリケーションのパフォーマンスが向上する。
spin-wait では通常 CPU の稼動は中断されないが、ブロッキングでは CPU はアイドル状態に
なる。しかし、ハイパー・スレッディング・テクノロジ対応 CPU の仮想プロセッサ上で
spin-wait を実行すると、その CPU 上の他の仮想プロセッサの稼動が中断される場合がある。
このようなアプリケーションには、インテル® コンパイラの OpenMP ライブラリが理想的で
ある。この OpenMP ランタイム・ライブラリは、ハイパー・スレッディング・テクノロジに
合わせて、スピン・パラメータを自動的に調整する。Windows クリティカル・セクション関
数も、ユーザがスピンカウントを制御できるため、このようなアプリケーションに有効であ
る(同期、4.1「ロックの競合と大小のクリティカル・セクションの管理」を参照)。
逆に、スループット重視のアプリケーション、すなわちアクティブ・スレッドの数がシステ
ム上のプロセッサ数を超えるアプリケーションでは、ブロッキング API を使用する方が、ス
ループットが全体として向上する。これは、ブロッキングの場合、アプリケーション内の他
の実行可能な状態のスレッドまたはシステム上の他のジョブが、ただちに実行されるからで
ある。Windows セマフォ、イベント、mutex 変数は、このクラスのアプリケーションに合っ
た機能を提供する。
最近のロッキング・アルゴリズムのほとんどは、spin-wait を無制限に実行することはない。
これらのアルゴリズムは、通常は「バックオフ」方式を採用し、一定時間スピンした後、
CPU を解放してオペレーティング・システムに渡す。リアルタイム・アプリケーションな
どの特殊な状況を除いて、メモリ操作によるユーザ独自のロックを設計する場合は、適切な
「バックオフ」方式を設計して、システム全体の動作の停滞を避ける必要がある。純粋な
spin-wait では、このような状態が起こる可能性がある。
ユーザ独自の spin-wait ループを設計する場合に注意すべきもう 1 つの点は、インテル ®
Pentium® 4 プロセ ッサ上で は spin-wait ループ内 で PAUSE 命 令を使用 すること である。
PAUSE 命令は、マルチプロセッサ構成で他のプロセッサが使用できるようにプロセッサ・バ
スを解放する、低レイテンシ命令である。ハイパー・スレッディング・テクノロジ対応 CPU
上では、PAUSE 命令によって、spin-wait を実行しても CPU 上の他の仮想プロセッサの稼動
が中断されにくくなる。spin-wait を実行してもプロセッサの稼動が中断されないシステムで
は、PAUSE 命令は無効である。
OpenMP アプリケーションの場合は、インテル・コンパイラを使用する。バックオフ・アル
ゴリズムを使用して spin-wait を実行するには、環境変数 KMP_LIBRARY=turnaround を設
定する。最終的に CPU をオペレーティング・システムに解放するバックオフ・アルゴリズ
ムを使用して spin-wait を実行するには、KMP_LIBRARY=throughput を使用する。
48
アプリケーションのスレッド化
3
使用ガイドライン
spin-wait 動作は、CPU サイクルを消費する。しかし、待機しているリソースを迅速に獲得で
きると予想される場合は、spin-wait はターンアラウンド時間の短縮に効果的な手法である。
他のスレッドからイベントまたは状態変数を介してウェークアップされるより、ロックを獲
得する方がはるかに高速である。しかし、長い待ち時間が予想される場合は、spin-wait を使
用すると、他のジョブの稼動が中断され、システム全体のパフォーマンスが低下するときが
ある。この問題を避けるために、spin-wait を使用する場合は、短時間(通常は約数百分の 1
秒)に限り使用するべきである。ハイパー・スレッディング・テクノロジ対応 CPU では、
複数の仮想プロセッサが実行リソースを共有しているため、spin-wait による無駄が特に大き
くなる。このようなシステムでは、spin-wait ループ内で PAUSE 命令を使用することと、
spin-wait カウントを非常に小さい値に調整することによって、仮想プロセッサの稼動の中断
を最小限に抑える必要がある。インテル・コンパイラの OpenMP ランタイム・ライブラリ
は、これらの調整を自動的に実行する。
参考資料
本書の参照個所 :
同期、4.1「ロックの競合と大小のクリティカル・セクションの管理」
同期、4.2「手作業でコーディングした同期ルーチンではなく、スレッド関連
の API が提供する同期ルーチンを使用する」
同期、4.3「同期用の Win32 アトミック、ユーザ空間ロック、カーネル・オブ
ジェクトの対比」
49
マルチスレッド・アプリケーションの開発
3.5 見かけの依存関係の回避または解消による並列性の露出
カテゴリ
アプリケーションのスレッド化
記述範囲
一般的なマルチスレッド処理、特にデータ分解と OpenMP*
キーワード
データ依存関係、コンパイラによる最適化、ブロッキング・アルゴリズム、Win32* スレッ
ド、OpenMP、Pthreads
摘要
多くのアプリケーションとアルゴリズムに含まれる逐次性能の最適化手法は、意図しない
データ依存関係を発生させ、並列実行の妨げになる可能性がある。開発者は、多くの場合、
簡単な変換によって、このような依存関係を解消できる。また、ドメイン分解またはブロッ
キングなどの手法によっても、依存関係を回避できる。
背景情報
並列実行のためのマルチスレッド化は、パフォーマンス向上の重要な源泉であるが、各ス
レッドが効率的に実行されるように保証することも、同じように重要である。この作業の大
部分は、コンパイラの最適化機能によって行われる。一方、プログラマがソースコードを修
正し、データの再利用とマシンの強みを活かした命令の選択によってパフォーマンスを向上
させることも、よく行われている。残念なことに、逐次性能を向上させる最適化手法によっ
て、意図しないデータ依存関係が発生し、マルチスレッド化によるパフォーマンスの向上の
妨げになる可能性がある。
その一例は、重複計算を避けるための中間結果の再利用である。例えば、画像にぼかしを入
れる処理を行うには、各画像ピクセルを、そのピクセルを含む隣接領域内のピクセルの加重
平均で置き換える。以下のコード例 3 は、3 × 3 のぼかしステンシルを記述する疑似コード
を示している。
コード例 3. 3 × 3 のぼかしステンシルを記述する疑似コード
for each pixel in (imageIn)
sum = value of pixel
// compute the average of 9 pixels from imageIn
for each neighbor of (pixel)
sum += value of neighbor
// store the resulting value in imageOut
pixelOut = sum / 9
各ピクセル値が複数の計算に使用されるため、開発者は、パフォーマンス向上のためにデー
タを再利用できる。以下の疑似コードでは、中間結果を計算して 3 回使用することにより、
逐次性能を向上させている。
50
アプリケーションのスレッド化
3
subroutine BlurLine(lineIn, lineOut)
for each pixel j in (lineIn)
// compute the average of 3 pixels from line
// and store the resulting value in lineout
pixelOut = (pixel j-1 + pixel j + pixel j+1) / 3
declare lineCache[3]
lineCache[0] = 0
BlurLine(line 1 of imageIn, lineCache[1])
for each line i in (imageIn)
BlurLine (line i+1 of imageIn, lineCache[i mod 3])
lineSums = lineCache[0] + lineCache[1] + lineCache[2]
lineOut = lineSums / 3
この最適化手法は、出力画像の隣接する行の計算の間に依存関係が発生する。このループの
反復を並行して計算しようとすると、この依存関係のために誤った結果が得られる。
もう 1 つの一般的な例は、ループ()内のポインタ・オフセットである。このコードは、ptr
をインクリメントすると、レジスタ・インクリメントの高速動作を利用し、反復ごとに
someArray[i] を計算する算術演算を避ける。compute の各呼び出しは互いに独立していて
も、各反復のポインタ値は前回の反復のポインタ値に依存するため、ポインタは明示的依存
関係を持つ。例えば、このループを OpenMP で並列化した場合、インテル・スレッド・チェッ
カーは、ptr の使用個所でメモリ競合をレポートする。
コード例 4. ループ内のポインタ・オフセット
ptr = &someArray[0];
for (i = 0; i < N; i++)
{
Compute (ptr);
ptr++;
}
最後に、アルゴリズムが並列性を要求しているにもかかわらず、データ構造が異なる目的で
設計されているため、意図に反して並列実行が妨げられる場合がある。疎行列アルゴリズム
がその一例である。ほとんどの行列要素が 0 であるため、通常の行列表現は、多くの場合
「パックド」形式で置き換えられる。パックド形式の行列は、要素の値と相対オフセットで
構成され、値が 0 のエントリを無視するために使用される。
この項目の目的は、このような難しい状況で、並列性を効果的に活かす方法を示すことであ
る。
推奨事項
もちろん、既存の最適化手法の削除やソースコードの大きな変更を行わずに、並列性を利用
する方法を見つけることがベストである。逐次性能の最適化手法を削除して並列性を露出さ
せる前に、問題全体の一部に既存のカーネルを適用して、最適化手法を残せないかどうかを
検討する。通常は、元のアルゴリズムに並列性が含まれている場合、問題の各部分を互いに
独立したユニットとして定義し、それらを並行して計算することも可能である。
ぼかし操作を効率的にスレッド化するには、画像全体を固定サイズの部分画像(すなわち、
ブロック)に分割する。ぼかしアルゴリズムは、データのブロックを互いに独立して計算で
きる。以下の疑似コードは、画像のブロック化の使用例を示している。
51
マルチスレッド・アプリケーションの開発
// One time operation:
// Decompose the image into non-overlapping blocks.
blockList = Decompose (image, xRes, yRes)
foreach (block in blockList)
{
BlurBlock (block, imageIn, imageOut)
}
画像全体にぼかしを入れる既存のコードは、BlurBlock のコーディングに再利用できる。
OpenMP または明示的スレッドを使用して、複数のブロックを並行して操作すれば、最適化
された元のカーネルを維持しながら、マルチスレッド処理のメリットが得られる。
既存の逐次性能の最適化のメリットが、各反復の全体的コストより小さいため、ブロック化
が不要になる場合もある。この条件は、多くの場合、反復の粒度が十分に粗く、並列化によ
るスピードアップが期待できる場合に該当する。ポインタのインクリメントの例は、その一
例である。誘導変数を明示的インデックスで置き換えれば、簡単に依存関係を解消し、ルー
プを並列化できる。
ptr = &someArray[0];
for (i = 0; i < N; i++)
{
Compute (ptr[i]);
}
ただし、元の(小さな)最適化手法は、必ずしも失われるとは限らない。コンパイラは、多
くの場合、インクリメントなどの高速操作を利用して、インデックス計算を積極的に最適化
する。これによって、逐次性能と並列性能の両方のメリットが得られる。
パックド疎行列を処理するコードなどの条件は、さらにスレッド化が難しくなる。通常、
データ構造をアンパックするのは実際的でないが、行列をブロックに分割し、各ブロックの
始点へのポインタを格納することは可能である。ブロック化された行列と、適切なブロック
ベースのアルゴリズムを組み合わせれば、パックド表現と並列性の両方のメリットが同時に
得られる。
上記のブロック化手法は、ドメイン分解と呼ばれる一般的な手法の一例である。ドメイン分
解の後、各スレッドは、1 つ以上のドメインを互いに独立して処理する。アルゴリズムと
データの性質によって、ドメイン当たりの仕事の量がほぼ一定になることも、ドメインごと
に仕事の量が変化することもある。ドメインごとに仕事の量が異なる場合、ドメインの数と
スレッドの数が等しいと、付加の不均衡によって並列性能が制限されるときがある。一般的
に、ドメインの数はスレッドの数より十分に大きいことが望ましい。ドメインの数が多けれ
ば、動的スケジューリングなどの手法で、スレッド間の負荷を平準化できる。
52
アプリケーションのスレッド化
3
使用ガイドライン
逐次性能の最適化手法には、パフォーマンスを大きく向上させるものがある。最適化が失わ
れることによるパフォーマンスの低下より、並列化によるスピードアップの効果が大きくな
るように保証するには、プロセッサの数を考慮する必要がある。
ブロック化アルゴリズムを使用すると、別名参照されるデータと別名参照されないデータを
区別するコンパイラの機能の妨げになるときがある。ブロック化の後、コンパイラが別名参
照されないデータを判別できなくなると、パフォーマンスが低下する。restrict キーワー
ドを使用して、別名参照を明示的に禁止することを推奨する(インテル® ソフトウェア開発
製品、2.1「インテル ® コンパイラによる自動並列化」を参照)。また、プロシージャ間の最
適化を有効にすると、コンパイラは別名参照されないデータを簡単に検出できる。
参考資料
本シリーズの参照個所 :
インテル® ソフトウェア開発製品、2.1「インテル® コンパイラによる自動並列化」
インテル® ソフトウェア開発製品、2.4「インテル® スレッド・チェッカーによ
るマルチスレッド・エラーの検出」
本章、3.2「粒度と並列性能」
本章、3.3「ロード・バランスと並列性能」
53
マルチスレッド・アプリケーションの開発
3.6 ワークロードの発見的手法による実行時の適切なスレッド数の
決定
カテゴリ
アプリケーションのスレッド化
記述範囲
一般的なマルチスレッド処理、OpenMP*、POSIX スレッド、Win32* スレッド
キーワード
ロード・バランス、粒度、Win32 スレッド、OpenMP、Pthreads
摘要
ほとんどのアプリケーションとワークロードのペアが処理する仕事の量は限られているた
め、マルチスレッド化によるスピードアップも限られている。適切なスレッド数の選択は、
マルチスレッド・アプリケーションのパフォーマンスの重要なポイントである。この項目で
は、適切なスレッド数を選択するための発見的手法の設計に関連する要因について説明す
る。
背景情報
機能上の理由でアプリケーションをスレッド化する場合、プログラマは、多くの場合、特定
の機能を特定のスレッドに割り当てる。また、すべてのスレッドが同時にアクティブになる
ことはまれである。機能的にスレッド化されたシステムでは、多くの場合、スレッドの数は
必要な機能に基づいて選択され、簡単には変化しない。幸運なことに、この選択の際に、通
常はパフォーマンスは重視されない。
しかし、性能上の理由でスレッド化されたアプリケーション(またはアプリケーションの一
部)では、多くの場合、1 つの問題に適用されるスレッドの数をプログラマが選択できる。
ほとんどのアプリケーションは、使用できるスレッドの数に制限がある。この制限は、実質
的に、スレッド化に関連する各種の暗黙的コストと明示的コストに基づいている。例えば、
暗黙的コストには、オペレーティング・システムにかかるスケジューリングの負担の増加、
スレッドへのデータ移動のコスト、(すべてのスレッドにデータを供給するために)システ
ムにかかるメモリ圧力の増加が含まれる。明示的コストには、スレッドのスタートアップ、
シャットダウン、調整が含まれる。1 つの問題に適用される適切なスレッド数の選択には、
これらのコストと仕事の量の組み合わせ、並列実行に利用できる独立したワークアイテムの
数、それらの粒度が重要な役割を演じる。
54
アプリケーションのスレッド化
3
オペレーティング・システムのスレッドを使用する場合は、プログラマは、希望するスレッ
ド数の作成と使用によって、直接この決定を行う。しかし、OpenMP を使用する場合は、プ
ログラマは、使用するスレッド数をシステムに決定させられる。ほとんどの OpenMP ライブ
ラリ(インテルの OpenMP ライブラリを含む)は、デフォルトでは、システム上のプロセッ
サの数に合わせてスレッドを作成する。ほとんどのアプリケーションでは、この選択がベス
トではない。この設定では、ハイパー・スレッディング・テクノロジ対応のシングル CPU
システムから 64 CPU 以上の SMP システムまで、利用可能な全範囲にわたる並列システム
に合わせたスケーラビリティは得られそうにない。
これらの理由で、使用するスレッド数をユーザに決定させるか、実行時の発見的手法または
測定によって計算とデータのサイズを理解した後に適切なスレッド数を選択する方法がベ
ストである。
推奨事項
アプリケーションのワークロードに影響を与える入力が、広い範囲で変化する場合は、使用
するスレッド数の決定を実行時まで延期する(実行時に入力のサイズを確認できる)。スレッ
ド数に影響を与えるワークロード入力パラメータの例には、行列のサイズ、データベースの
サイズ、画像 / ビデオのサイズと解像度、ツリーベースの構造の深さ / 幅 / 密度、リストベー
スの構造のサイズなどが含まれる。
同様に、アプリケーションを実行するシステム上のプロセッサ数がさまざまに異なる場合
は、使用するスレッド数の決定をアプリケーションの実行時まで延期する(実行時にマシン
のサイズを確認できる)
。
上記のワークロードとシステムサイズの入力を使用して、経験的なデータに基づいて発見的
手法を開発し、アプリケーションの実行時にスレッド数を設定する。
入力データからアプリケーションの仕事の量を予測できない場合は、測定手順を使用して
ワークロードとシステムの特性を理解し、その結果に基づいて適切なスレッド数を選択す
る。測定手順にコストがかかる場合は、ファイルシステムなどの永続的な場所に格納するこ
とで、測定手順を永続的に利用できる。
すべてのスレッドが同時にアクティブになる可能性がある場合は、システム上のプロセッサ
数を超える数のスレッドを作成しないようにする。この状態では、オペレーティング・シス
テムがプロセッサを多重使用するため、通常は最適なパフォーマンスは得られない。
アプリケーション全体ではなくライブラリを開発する場合は、ライブラリが使用するスレッ
ド数を、ライブラリのユーザが簡単に選択できるメカニズムを提供する。これは、ユーザが
より高水準の並列実行機能を持っているため、ライブラリ内の並列実行機能が不要になる可
能性があるためである。
最後に、OpenMP では、並列リージョンで num_threads 節を使用して、使用されるスレッ
ド数を制御する。また、並列リージョンで if 節を使用して、マルチスレッドを使用するか
どうかを決定する。omp_set_num_threads 関数も使用できるが、よく理解している特殊
な状況以外では、この関数の使用は推奨できない。この関数の影響はグローバルであり、現
在の関数の終了後も持続するため、呼び出しツリー内の親ルーチンに影響を与える可能性が
ある。num_threads 節の影響はローカルであり、呼び出し元の環境には影響を与えない。
55
マルチスレッド・アプリケーションの開発
使用ガイドライン
新しい世代のコンピュータ・システムが現れるたびに、暗黙的コストと明示的コストは変化
する。これは、CPU とメモリの速度の比、各種のアルゴリズム、システムの構成レイアウ
ト(単純な SMP システム、マルチスレッド化された SMP システム、NUMA システム、そ
れぞれの組み合わせ)などの基礎となる条件が変化するためである。これらの変化のため
に、使用されるスレッド数の再評価が必要になる。細かい粒度の並列性を含むアプリケー
ションは、特にここに挙げた項目の影響を受けやすいため、スレッド数の決定に非常に手間
がかかる。粗い粒度の並列性を含むアプリケーションは、この点ではより安定している。
従って、粗い粒度の使用を推奨する。
ここで検討したアプリケーション固有の要因以外に、コンピューティング環境にも注意する
必要がある。1 つのアプリケーションだけを実行する専用システムと、他のジョブも共有す
るシステムでは、スレッド数選択のための発見的手法が全く異なる場合がある。
参考資料
本シリーズの参照個所 :
インテル® ソフトウェア開発製品、2.4「スレッド・プロファイラによる
OpenMP パフォーマンスの評価」
本章、3.2「粒度と並列性能」
本章、3.3「ロード・バランスと並列性能」
本章、3.4「ターンアラウンド重視のスレッド化とスループット重視のスレッ
ド化」
56
アプリケーションのスレッド化
3
3.7 スレッドプールによるシステム・オーバーヘッドの削減
カテゴリ
アプリケーションのスレッド化
記述範囲
一般的なマルチスレッド処理
キーワード
スレッドプール、システム・オーバーヘッド、Win32* スレッド、OpenMP*、Pthreads
摘要
多くのスレッド・アプリケーションは、スレッド・オンデマンド・ポリシーでスレッドを管
理する。このポリシーでは、スレッドは必要に応じて作成され、使用後は直ちに削除され
る。このポリシーの主な利点は、コーディングとスレッド管理が簡単に行えることである。
しかし、実行中に多くのスレッドを作成すると、オペレーティング・システムがスレッドを
作成できない場合に備えて、プログラムの制御ロジックが複雑になる。多くのアプリケー
ションは、このような障害の可能性を無視しており、安全性が不十分である。さらに、ス
レッド作成のコストはかなり大きいため、頻繁にスレッドを作成すると、性能上のペナル
ティが生じる。例えば、サーバ・アプリケーションなど、多くのスレッドを処理するアプリ
ケーションでは、スレッド管理のコストが非常に大きくなるときがある。スレッドの数が増
えるほど、スレッドの作成、終了、スケジューリング、コンテキスト・スイッチングのコス
トが増加し、ついにはシステム・オーバーヘッドがマルチスレッド化のメリットを超えてし
まう。
背景情報
スレッドプールは、コスト・パフォーマンスの高いスレッド管理方法である。スレッドプー
ルとは、仕事の割り当てを待っているスレッドのグループである。この方法では、スレッド
は初期化手順中に一度作成され、終了手順中に終了する。これによって、アプリケーション
の実行途中でスレッド作成の失敗がないかどうかチェックする制御ロジックが簡単になり、
スレッド作成のコストがアプリケーション全体に分散される。一度作成されたスレッドは、
スレッドプール内で仕事が利用可能になるのを待機する。アプリケーション内の他のスレッ
ドが、スレッドプールにタスクを割り当てる。通常は、スレッド・マネージャまたはディス
パッチャと呼ばれる 1 つのスレッドがこれを行う。タスクの完了後、各スレッドはスレッド
プールに戻り、次の仕事を待つ。仕事の割り当てとスレッドプール・ポリシーによっては、
仕事の量が増えた場合、スレッドプールに新しいスレッドを追加することが可能である。こ
の方法には、次のような明らかな利点がある。
•
簡単な制御ロジックによって、スレッド作成の失敗による、アプリケーションの実
行途中でのランタイム障害を回避できる。
•
スレッドの作成によって発生するスレッド管理コストが最小限に抑えられる。その
結果、ワークロード処理の応答時間が向上し、より細かい粒度のワークロードのマ
ルチスレッド化が可能になる(本章、3.2「粒度と並列性能」を参照)。
57
マルチスレッド・アプリケーションの開発
スレッドプールの一般的な使用条件には、サーバ・アプリケーションがある。サーバ・アプ
リケーションは、多くの場合、新しい要求があるたびにスレッドを起動する。もっと良い方
法は、サービス要求をキューに入れておき、既存のスレッドプールに処理させることであ
る。プール内のスレッドは、キューからサービス要求を取得し、その要求を処理した後、
キューに戻って次の仕事を得る。
また、スレッドプールを使用して、オーバーラップ非同期 I/O を実行できる。Win32 API で
提供される I/O 完了ポートによって、スレッドプールは、I/O 完了ポートを待機し、オーバー
ラップ I/O 操作からのパケットを処理できる。
OpenMP は、厳密な fork/join スレッドモデルである。一部の OpenMP ライブラリは、並列
リージョンの始まりでスレッドを作成し、並列リージョンの終わりで削除する。OpenMP ア
プリケーションは、通常は複数の並列リージョンを持ち、その間に Serial リージョンが存在
する。各並列リージョンでスレッドの作成と削除を実行すると、特に並列リージョンがルー
プの中にある場合、大きなシステム・オーバーヘッドが発生する。従って、インテル OpenMP
ライブラリは、スレッドプールを使用する。最初の並列リージョンで、ワーカスレッドの
プールが作成される。これらのスレッドは、プログラムが実行されている間存在する。プロ
グラムの要求に応じて、スレッドが自動的に追加される。最後の並列リージョンが実行され
るまで、これらのスレッドは削除されない。
Windows* および Linux* では、スレッド作成 API を使用してスレッドプールを作成できる。
例えば、Win32 スレッドを使用するカスタム・スレッドプールは、次のように作成できる。
// Initialization method/function
{
DWORD tid;
//
// Create initial pool of threads
//
for (int i = 0; i < MIN_THREADS; i++)
{
HANDLE *ThHandle = CreateThread (NULL,
0,
CheckPoolQueue,
NULL,
0,
&tid);
if (ThHandle == NULL)
// Handle Error
else
RegisterPoolThread (ThHandle);
}
}
プール内の各スレッドが実行する関数 CheckPoolQueue は、キュー内の仕事が利用可能に
なるまで、待機状態に入るように設計されている。スレッド・マネージャは、キュー内の未
処理のジョブを監視し、必要に応じて動的にプール内のスレッド数を増加させる。
58
アプリケーションのスレッド化
3
推奨事項
スレッドプールを使用して、スレッド管理のオーバーヘッドを最小限に抑え、アプリケー
ションのパフォーマンス(すなわち、スループット、応答時間、スケーラビリティ)を向上
させる。
インテル OpenMP ライブラリは、オーバーヘッドを最小限に抑えるために、すでにスレッド
プールを使用している。OpenMP は、シンクロナス・スレッド・アプリケーション(特に
データ並列アプリケーション)に最適である(本章、3.1「適切なスレッド化手法の選択 :
OpenMP と明示的スレッド化」を参照)。
スレッドプールと I/O 完了ポートを組み合わせて、Windows アプリケーション内の非同期
I/O パフォーマンスを向上させる。
アプリケーションは、スレッド関連の Win32 API または POSIX API を使用して、スレッド
プールの作成と管理を実行できる。標準的なスレッドプール関数 / クラスは、Win32、C# in
.Net、Java*、RogueWave で利用できる。
参考資料
本書の参照個所 :
本章、3.1「適切なスレッド化手法の選択 : OpenMP と明示的スレッド化」
本章、3.2「粒度と並列性能」
本章、3.4「ターンアラウンド重視のスレッド化とスループット重視のスレッ
ド化」
他の参考資料 :
Win32 API:
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dllproc/base/queueuse
rworkitem.asp(英語)
C# with .Net:
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfSystem
ThreadingThreadPoolClassTopic.asp(英語)
RogueWave:
http://www.roguewave.com/support/docs/sourcepro/threadsug/3-7.html(英語)
59
マルチスレッド・アプリケーションの開発
3.8 順序付けされたデータ・ストリーム内のデータ並列性の利用
カテゴリ
アプリケーションのスレッド化
記述範囲
一般的なマルチスレッド処理、任意のプログラミング言語またはオペレーティング・システ
ム
キーワード
データ並列性、I/O、順序依存性
摘要
大量の演算を必要とするアプリケーションは、多くの場合、順序付けされた入力データから
順序付けされた出力データへの複雑な変換を実行する。この例には、音声とビデオのトラン
スコード、可逆データ圧縮、地震データの処理などがある。多くの場合、これらの変換に使
用されるアルゴリズムは並列アルゴリズムであるが、I/O 順序の依存性を管理するのは難し
い課題である。この節では、いくつかの課題を特定し、並列性能を犠牲にせずにそれらの課
題に対処する方法を示す。
背景情報
ライブ・ビデオ・ソースからディスクまたはネットワーク・クライアントへの非圧縮ビデオ
のリアルタイム圧縮処理を実行するように設計された、ビデオ圧縮エンジンをスレッド化す
る問題を考える。明らかに、このようなアプリケーションのリアルタイムの必要条件を満た
す鍵は、マルチプロセッサのパワーを活用することである。
MPEG-2 や MPEG-4 などのビデオ圧縮標準は、信頼性の低いリンクを介したストリーミング
向けに設計されている。従って、1 つのビデオ・ストリームを、一連の小さい独立したスト
リームとして簡単に扱える。これらの小さいストリームを並行して処理すれば、大幅なス
ピードアップが可能になる。マルチスレッド化によってこの並列性を利用する場合、次のよ
うな課題がある。
1. 問題を互いに重ならない部分に分割し、それらをスレッドに割り当てる。
2. 入力データが正しい順序で一度だけ読み込まれるように保証する。
3. 大きな性能上のペナルティなしで、処理が実際に完了した順序に関係なく、正しい
順序でブロックを出力する。
4. 入力データの実際の範囲に関する事前の知識なしで、上記の手順を実行する。
可逆データ圧縮などの他の状況では、多くの場合、入力データのサイズを前もって確認し、
入力データを互いに独立した入力ブロックとして直接分割できる。この場合も、ここで説明
する手法を同じように適用できる。
60
アプリケーションのスレッド化
3
推奨事項
一連のデータ生産者と消費者を作成する方法では、スケーラビリティが得られず、負荷の不
均衡の影響を受けやすい。代わりに、データ分解を使用して、上記の課題に対処し、よりス
ケーラブルな設計を実現する。
基本的な方法は、スレッドのチームを作成し、チーム内の各スレッドが、ビデオブロックの
読み込み、ブロックのエンコード、リオーダー・バッファへの出力を実行する。各ブロック
の処理が完了すると、スレッドは、次のビデオブロックの読み込みと処理に戻る。
仕事を動的に割り当てれば、負荷の不均衡が最小限に抑えられる。リオーダー・バッファ
は、符号化されたビデオのブロックが、処理が完了した順序に関係なく、正しい順序で書き
出されるように保証する。
元のビデオ・エンコード・アルゴリズムは、次のような形式である。
inFile = OpenFile ()
outFile == InitializeOutputFile ()
WriteHeader (outFile)
outputBuffer = AllocateBuffer (bufferSize)
while (frame = ReadNextFrame (inFile))
{
EncodeFrame (frame, outputBuffer)
if (outputBuffer size > bufferThreshold)
FlushBuffer(outputBuffer, outFile)
}
FlushBuffer (outputBuffer, outFile)
最初の手順は、フレーム・シーケンスの読み込みとエンコードを、ブロックベースのアルゴ
リズムで置き換えることである。これによって、スレッドのチーム内での分解のための問題
が設定される。
WriteHeader (outFile)
while (block = ReadNextBlock (inFile))
{
while(frame = ReadNextFrame (block))
{
EncodeFrame (frame, outputBuffer)
if (outputBuffer size > bufferThreshold)
FlushBuffer (outputBuffer, outFile)
}
FlushBuffer (outputBuffer, outFile)
}
データブロックの定義はアプリケーションによって異なるが、ビデオ・ストリームの場合
は、最小および最大ブロックサイズの制約条件に従って、入力内でシーンの変化が最初に検
出されるフレームが、ブロックの自然境界になる。ブロックベースの処理では、入力バッ
ファの割り当てとソースコードの多少の変更によって、処理の前にバッファを充填する必要
がある。さらに、ReadNextFrame の方法を、(ファイルからの読み込みから)バッファか
らの読み込みに変更する必要がある。
61
マルチスレッド・アプリケーションの開発
次の手順は、出力のバッファ方法を変更し、ブロック全体が 1 つのユニットとして書き込ま
れるように保証することである。この方法は、ブロックが正しい順序で出力されるように保
証するだけで済むため、出力のリオーダーが非常に簡単になる。以下のコードは、ブロック
ベースの出力への変更を反映している。
WriteHeader (outFile)
while (block = ReadNextBlock (inFile))
{
while (frame = ReadNextFrame (block))
{
EncodeFrame (frame, outputBuffer)
}
FlushBuffer (outputBuffer, outFile)
}
最大ブロックサイズによっては、もっと大きな出力バッファが必要になる。
各ブロックは互いに独立しているため、通常は特殊なヘッダで各出力ブロックを開始する。
MPEG ビデオ・ストリームの場合、このヘッダの後に I- フレームと呼ばれる完全なフレー
ムが続き、このフレームを基準として将来のフレームが定義される。従って、このヘッダ
は、複数のブロックにわたるループの中に移される。
while (block = ReadNextBlock (inFile))
{
WriteHeader (outputBuffer)
while (frame = ReadNextFrame (block))
{
EncodeFrame (frame, outputBuffer)
}
FlushBuffer (outputBuffer, outFile)
}
これらの変更によって、スレッド・ライブラリ(Pthreads またはスレッド関連の Win32* API)
または OpenMP* 並列セクション2 を使用した、並列スレッドの導入が可能になる。
// Create a team of threads with private copies of outputBuffer,
// block, and frame and shared copies of inFile and outFile
while (AcquireLock,
block = ReadNextBlock (inFile),
ReleaseLock, block)
{
WriteHeader (outputBuffer)
while (frame = ReadNextFrame (block))
{
EncodeFrame (frame, outputBuffer)
}
FlushBuffer (outputBuffer, outFile)
}
2.
62
インテル WorkQueue の OpenMP 拡張機能を使用すれば、このコードはさらに簡単になる。
アプリケーションのスレッド化
3
この方法は、データを正しい順序で安全に読み込むための簡単で効果的な方法である。各ス
レッドは、ロックの獲得、データブロックの読み込み、ロックの解放を実行する。入力ファ
イルの共有によって、データブロックは正しい順序で一度だけ読み込まれる。実行可能な状
態のスレッドは常にロックを獲得するため、データブロックは、first-come-first-served(FCFS)
方式で動的にスレッドに割り当てられる。これによって、通常は負荷の不均衡が最小限に抑
えられる。
最後の手順は、ブロックが正しい順序で安全に出力されるように保証することである。簡単
な方法は、ロックと共有出力ファイルを使用して、ブロックが一度に 1 つずつ書き込まれる
ようにすることである。この方法では、スレッドの安全は保証されるが、ブロックが元の順
序と異なる順序で出力される可能性がある。
また、各スレッドは、それまでのブロックの書き込みがすべて完了するまで、自分の出力を
フラッシュできなくなる。残念なことに、この方法は、各スレッドは書き込みの順番が来る
までアイドル状態で待たなければならないため、効率性が低下する。
より効果的な方法としては、出力ブロックの循環リオーダー・バッファを設置することであ
る3。各ブロックには通し番号が割り当てられる。バッファの「末尾」は、次に書き込まれ
るブロックを指定する。あるスレッドが処理を完了したデータブロックが、バッファの末尾
によって指定されるブロックと一致しない場合、そのスレッドは、単にそのブロックを適切
なバッファ内の位置に入れ、次の利用可能なブロックの読み込みと処理に戻る。同様に、あ
るスレッドが処理を完了したブロックが、バッファの末尾によって指定されるブロックと一
致した場合、そのスレッドは、そのブロックとすでにキューに入れられていた連続ブロック
の書き出しを実行する。最後に、スレッドは、バッファの末尾が次の出力ブロックを指すよ
うに、更新する。リオーダー・バッファは、処理が完了したブロックがインオーダーで書き
出されることを保証している間は、これらのブロックをアウト・オブ・オーダーでキューに
入れることを許可している。
図 9.
書き出し前のリオーダー・バッファの状態の例
ᧃየ
0
ࡉࡠ࠶ࠢ
3.
1
NULL
2
3
ࡉࡠ࠶ࠢ
NULL
4
NULL
5
6
7
ࡉࡠ࠶ࠢ ࡉࡠ࠶ࠢ ࡉࡠ࠶ࠢ
この方法は、命令がアウト・オブ・オーダーで処理され、インオーダーでリタイアされる、一部のマイクロ
プロセッサで使用されるリオーダー・バッファによく似ている。
63
マルチスレッド・アプリケーションの開発
図 9 は、リオーダー・バッファの状態の 1 例を示している。ブロック 0 ~ 35 は、既に処理
と書き出しが完了している。ブロック 37、38、39、40、42 は、処理が完了し、書き出しの
ためにキューに入れられている。ブロック 36 を処理しているスレッドは、処理を完了する
と、ブロック 36 ~ 40 を書き出す。これで、リオーダー・バッファは図 10 に示す状態にな
る。ブロック 41 の処理が完了するまで、ブロック 42 はキューに入れられたままになる。
図 10. 書き出し後のリオーダー・バッファの状態の例
ᧃየ
0
NULL
1
NULL
2
3
NULL
4
NULL
5
NULL
6
NULL
7
NULL
ࡉࡠ࠶ࠢ
もちろん、このアルゴリズムの安定性と高速性を保証するには、以下の点に注意する必要が
ある。
•
共有されるデータ構造は、読み込みまたは書き込みの時点でロックされなければな
らない。
•
•
バッファ内のスロットの数は、スレッドの数を超えてはならない。
バッファ内の適切なスロットが利用できない場合、スレッドは効率的に待機しなけ
ればならない。
•
1 スレッドにつき複数の出力バッファをあらかじめ割り当てる。これによって、バッ
ファへのポインタをキューに入れ、不要なデータコピーとメモリ割り当てを避けら
れる。
出力キューを使用する場合、最終的なアルゴリズムは次のようになる。
inFile = OpenFile ()
outFile == InitializeOutputFile ()
// Create a team of threads with private
// copies of outputBuffer, block, and frame, shared
// copies of inFile and outFile.
while (AcquireLock,
block = ReadNextBlock (inFile),
ReleaseLock, block)
{
WriteHeader (outputBuffer)
while (frame = ReadNextFrame (block))
{
EncodeFrame (frame, outputBuffer)
}
QueueOrFlush (outputBuffer, outFile)
}
このアルゴリズムは、インオーダー I/O を保証し、しかも高性能における柔軟性とアウト・
オブ・オーダーの並列処理環境も提供している。
64
アプリケーションのスレッド化
3
使用ガイドライン
場合によっては、データの読み込みと書き込みに要する時間が、データの処理に必要な時間
に相当するほど長くなる。このような場合は、以下の手法が効果的である。
非同期 I/O – Linux* と Windows* は、データの読み込みまたは書き込みを開始した後でその操
作の完了を待機するか通知する API を備えている。これらのインターフェイスを使用して、
他の演算を実行しながら入力データの「プリフェッチ」と出力データの「ポストライト」を
行うと、I/O のレイテンシを効果的に隠蔽できる。Windows では、FILE_FLAG_OVERLAPPED
属性を指定すれば、ファイルが非同期 I/O に対してオープンされる。Linux では、非同期動作
は、libaio によって提供される多くの aio_* 関数によって実行される。
入力データの量が非常に大きい場合、静的分解手法では、ハードウェアが多数の非連続ブ
ロックの同時読み込みを実行しようとするため、物理ディスクの「スラッシング」が発生す
る と き が あ る。上 記 の 推 奨 事 項 に 従 っ て、共 有 フ ァ イ ル 記 述 子 と 動 的 な
"first-come-first-served" スケジューリング・アルゴリズムを使用すると、連続したブロックの
読み込みをインオーダーで実行でき、I/O サブシステムのスループットが全体として向上す
る。
データブロックのサイズと数は、慎重に選択する必要がある。通常は、ブロックの数が多い
ほど、スケジューリングの柔軟性が高まり、負荷の不均衡を軽減できる。しかし、ブロック
のサイズが小さすぎると、不要なロッキングのオーバーヘッドが発生し、データ圧縮アルゴ
リズムの有効性の妨げになることがある。スレッドの数を基準としたブロックの数とサイズ
の選択方法については、本書の「ロード・バランスと並列性能」および「粒度と並列性能」
の節を参照のこと。
参考資料
本シリーズの参照個所 :
インテル® ソフトウェア開発製品、2.4「インテル® スレッド・チェッカーによ
るマルチスレッド・エラーの検出」
本章、3.2「粒度と並列性能」
同期、4.1「ロックの競合と大小のクリティカル・セクションの管理」
同期、4.4「できるだけノンブロッキング・ロックを使用する」
65
マルチスレッド・アプリケーションの開発
3.9 ループ・パラメータの操作による OpenMP* パフォーマンスの最
適化
カテゴリ
アプリケーションのスレッド化
記述範囲
任意のオペレーティング・システム上の OpenMP* アプリケーション
キーワード
ループの最適化、粒度、ロード・バランス、OpenMP、バリア
摘要
データ並列アプリケーションは、互いに独立した同一の操作を、異なるデータに繰り返し実
行する。ループは、通常は、データ並列アプリケーション内で大量の演算を必要とするセグ
メントである。従って、ループの最適化は、パフォーマンスに直接影響を与える。
背景情報
ループの最適化は、データ並列アプリケーションのパフォーマンスを向上させるチャンスで
ある。ループ融合、ループ交換、ループのアンロールなどのループ最適化手法は、通常は、
同期オーバーヘッドなどの並列オーバーヘッドを最小限に抑えながら、粒度、ロード・バラ
ンス、データ局所性を向上させることを目的としている。一般的に、高いトリップカウント
を持つループは、並列化の候補として最適である。トリップカウントが高いほど、スレッド
間に配分されるタスクの利用可能性が大きくなり、より良い負荷バランスが可能になる。た
だし、ループの反復ごとの仕事の量も、考慮すべき要因である。ここでは、特に断らない限
り、各反復の仕事の量は、同じループ内の他の反復と(ほぼ)同じとする。
図 11 に示すように、OpenMP の for ワークシェアリング構文を使用するループの場合を考
える。この条件で、ループの反復を 4 つのスレッドに配分する場合、トリップカウントが低
いと、負荷の不均衡が発生する(本章、3.3「ロード・バランスと並列性能」を参照)
。1 回
の反復に数秒しかかからない場合は、この不均衡は大きな影響を与えない。しかし、各反復
に 1 時間かかる場合は、1 つのスレッドが仕事を続ける間、他の 3 スレッドは 60 分間アイ
ドル状態になる。この条件を、4 つのスレッドで 1 時間の反復を 1003 回実行する場合と対
照する。この場合は、10 日間の実行後の 1 時間のアイドル時間は問題にならない。
図 11. 低いトリップカウントのループを並列化すると、負荷の不均衡が発生する
#pragma omp for
for (i = 0; i < 13; i++)
{
// Computation
}
66
アプリケーションのスレッド化
3
推奨事項
ネストされたループを乗算する場合は、安全に並列化できるループのうち最も外側のループ
を選択する。一般的に、これによって最も粗い粒度が得られる(本章、3.2「粒度と並列性
能」を参照)。各スレッドに仕事が均等に配分されるように注意すること。最も外側のルー
プのトリップカウントが低いため、そのループを使用できない場合は、高いトリップカウン
トを持つ内側ループがスレッド化の候補になる。
void copy (int imx, int jmx, int kmx,
double**** w, double**** ws)
{
for (int nv = 0; nv < 5; nv++)
for (int k = 0; k < kmx; k++)
for (int j = 0; j < jmx; j++)
for (int i = 0; i < imx; i++)
ws[nv][k][j][i] = w[nv][k][j][i];
}
スレッド数が 5 の場合以外は、外側ループを並列化すると、必ず負荷の不均衡とアイドル状
態のスレッドが生じる。配列の次元 imx、jmx、kmx が非常に大きい場合は、この非効率は
特に重大になる。このような場合は、内側ループを並列化することを推奨する。
並列性能を向上させるもう 1 つの最適化手法は、ネストされたループを結合して反復回数を
増やすことである。例えば、トリップカウントが 8 と 9 の 2 つのネストされたループを結合
して、72 回反復される 1 つのループを作成できる(図 12)
。ただし、配列のインデックス操
作に両方のループカウンタを使用している場合は、新しいループカウンタを変換して、対応
するインデックス値に戻さなければならない。これによって、元のネストされたループには
なかった操作が追加される。しかし、この作業の多少の増加は、単一ループによるオーバー
ヘッドの削減と、2 つのループを 1 つのループに結合することによって顕在化される並列性
の向上によって埋め合わせられる。
図 12. ネストされたループを結合してトリップカウントを増やすと、並列性が顕在化さ
れ、パフォーマンスが向上する
#pragma omp parallel for
for (i = 0; i < 8; i++)
for (j = 0; j < 9; j++)
a[i][j] = b[j] *
c[i];
#pragma omp parallel for
for (ij = 0; ij < 72;
ij++)
{
int i = ij / 9;
int j = ij % 9;
a[i][j] = b[j] * c[i];
}
OpenMP ワークシェアリング構文の終わりの暗黙的バリアを回避しても安全な場合は、この
バリアを避ける。すべての OpenMP ワークシェアリング構文(for、sections、single)
は、構造化されたブロックの終わりに暗黙的バリアを持つ。すべてのスレッドは、実行を先
に進める前に、このバリアの位置でランデブーしなければならない。多くの場合、これらの
バリアは不要であり、パフォーマンスに悪影響を与える。以下の例に示すように、OpenMP
の nowait 節を使用して、このバリアを無効にできる。
67
マルチスレッド・アプリケーションの開発
void copy (int imx, int jmx, int kmx,
double**** w, double**** ws)
{
#pragma omp parallel shared(w, ws)
{
for (int nv = 0; nv < 5; nv++)
for (int k = 0; k < kmx; k++) // kmx is usually small
#pragma omp for shared(nv, k) nowait
for (int j = 0; j < jmx; j++)
for (int i = 0; i < imx; i++)
ws[nv][k][j][i] = w[nv][k][j][i];
}
}
最も内側のループの中の演算はすべて互いに独立しているため、各スレッドが次の k 反復に
進む前に、暗黙的なバリアの位置で待つ理由はない。反復ごとの仕事の量が不均等な場合、
nowait 節によって、スレッドは、暗黙的バリアの位置でアイドルにならずに、有益な仕事
を進められる。
OpenMP の if 節を使用して、ランタイム情報に基づいて、逐次実行または並列実行を選択
する(本章、3.6「ワークロードの発見的手法による実行時の適切なスレッド数の決定」を
参照)。場合によっては、ループの反復回数が実行時までわからないときがある。OpenMP
の parallel リージョンを複数のスレッドで実行することでパフォーマンスに悪影響を与
える場合(例えば、反復回数が少ない場合)は、最小しきい値の指定によってパフォーマン
スを維持できる。
#pragma omp parallel for if (N >= threshold)
for (i = 0; i < N; i++)
{
// Computation
}
このコード例は、ループの反復回数が、プログラマが指定したしきい値を超える場合にの
み、ループを並列実行する。
よく似たインデックスを持つ並列ループを融合して、オーバーヘッドを最小限に抑えなが
ら、粒度とデータ局所性を向上させる。図 13 では、最初の 2 つのループ(左側のコード例)
を簡単に結合できる(右側のコード例)。これらのループを結合すると、反復ごとの仕事の
量(すなわち、粒度)が大きくなり、ループのオーバーヘッドが小さくなる。3 番目のルー
プは、反復回数が異なるため、簡単には結合できない。さらに重要なことに、3 番目のルー
プと最初の 2 つのループの間には、データ依存関係が存在する。
図 13. よく似たインデックスを持つ並列ループを融合すると、粒度とデータ局所性が向上
する
for (j
a[j]
for (j
d[j]
=
=
=
=
0; j
b[j]
0; j
e[j]
<
+
<
+
N; j++)
c[j];
N; j++)
f[j];
for (j = 5; j < N ? 5;
j++)
g[j] = d[j+1] + a[j+1];
68
for (j
{
a[j]
d[j]
}
for (j
j++)
g[j]
= 0; j < N; j++)
= b[j] + c[j];
= e[j] + f[j];
= 5; j < N ? 5;
= d[j+1] + a[j+1];
アプリケーションのスレッド化
3
参考資料
本シリーズの参照個所 :
インテル® ソフトウェア開発ツール、2.4「インテル® スレッド・チェッカーに
よるマルチスレッド・エラーの検出」
インテル® ソフトウェア開発ツール、2.5「スレッド・プロファイラによる
OpenMP パフォーマンスの評価」
本章、3.2「粒度と並列性能」
本章、3.3「ロード・バランスと並列性能」
本章、3.6「ワークロードの発見的手法による実行時の適切なスレッド数の決定」
69
マルチスレッド・アプリケーションの開発
70
4
同期
スレッド化されたアプリケーションの実行中のレース状態を避けるには、共有リソースに排
他制御を適用し、共有リソースにアクセスして状態を変更できるスレッドを一度に 1 つに制
限する必要がある。共有リソースは、データ構造か、アドレス空間内のメモリである。アプ
リケーションのパフォーマンスには、同期のためのオーバーヘッドを最小限に抑えることが
重要である。本章では、マルチスレッド・アプリケーションの効果的な同期手法について説
明する。
マルチスレッド・アプリケーションでは、共有リソースにアクセスするコード・セクション
(クリティカル・セクション)を 1 つのスレッドが実行している間、競合する他のスレッド
はスピンするか、キュー内で待機する。すべての競合するスレッド間でロック制御のスケ
ジューリングについて公平性を保証するためには、クリティカル・セクション内のスレッド
の所要時間を最小限に抑えることが重要である。これは通常、クリティカル・セクション内
のコードのサイズを、ステート変更の処理に必要な最小限まで切り詰めることを意味する。
本章の最初の項目では、クリティカル・セクションの最適なサイズを決定するための設計上
の問題について説明する。
標準的なスレッド化ライブラリは、特定のアーキテクチャ向けに最適化され、広範囲にわた
るアプリケーション条件でテストされた同期プリミティブを提供する。通常は、これらのプ
リミティブには、最適化された spin-wait と効率的なスケジューリング・アルゴリズムが含
まれているため、同期とスケジューリングのためのオーバーヘッドが最小限に抑えられる。
さらに、標準的なスレッド化ライブラリ内の同期プリミティブは移植可能で、通常は上方互
換性と下方互換性を持ち、プラットフォーム間の簡単な移行が可能である。本章の 2 番目の
節では、手作業でコーディングした同期関数と比較した標準的なスレッド関連の API の優位
性について説明する。
マルチスレッド関連の Windows* API は、複数の同期プリミティブを提供する。これには、
クリティカル・セクション、mutex、セマフォ、イベント、インターロック操作が含まれる。
これらのプリミティブは、すべて排他制御を実現するが、それぞれに異なる性能上のメリッ
トと使用モデルを持つ。各種の同期プリミティブの比較については、次章「メモリ管理」で
扱う。
ほとんどのスレッド化ライブラリは、ブロック型のスレッド化プリミティブよりも効率的な
代替手段である、ノンブロッキング・スレッド化プリミティブを用意している。ノンブロッ
キング・スレッド化コールについては、次の項目で解説する。
本章の最後の節では、初期化、ファイルのオープン / クローズ、動的メモリ割り当てなど、
一度だけ実行されるイベントのロック獲得コストを最小限に抑えるために、ダブルチェッ
ク・パターン・ロックを使用するメリットについて説明する。
71
マルチスレッド・アプリケーションの開発
4.1 ロックの競合と大小のクリティカル・セクションの管理
カテゴリ
同期
記述範囲
一般的なマルチスレッド処理
キーワード
ロックの競合、同期、spin-wait、クリティカル・セクション、ロックのサイズ
摘要
マルチスレッド・アプリケーションでは、ロックを使用して、共有リソースにアクセスする
コード領域への移行を同期する。これらのロックで保護されるコード領域は、クリティカ
ル・セクションと呼ばれる。1 つのスレッドがクリティカル・セクションに入っている間は、
他のスレッドはクリティカル・セクションに入れない。従って、クリティカル・セクション
は、実行をシリアル化する。この項目では、クリティカル・セクションのサイズ(クリティ
カル・セクション内の 1 つのスレッドの所要時間)の概念と、それがパフォーマンスに与え
る影響について説明する。
背景情報
クリティカル・セクションは、複数のスレッドが共有リソースにアクセスしようとしたとき
のデータの整合性を保証する。また、クリティカル・セクションは、クリティカル・セク
ション内のコードの実行をシリアル化する。他のスレッドがロックの獲得を待ってアイドル
でいる時間(ロックの競合)を短縮するために、クリティカル・セクション内のスレッドの
所要時間をできるだけ短縮する必要がある。つまり、クリティカル・セクションは、できる
だけ小さいサイズに抑えることが望ましい。しかし、小さなクリティカル・セクションを多
数使用すると、各ロックの獲得と解放のためのシステム・オーバーヘッドが発生し、マルチ
スレッド化による性能上のメリットを相殺してしまう。この場合は、大きめのクリティカ
ル・セクションを 1 つだけ使用するのがベストである。大きなクリティカル・セクションの
使用が推奨される条件と小さなクリティカル・セクションの使用が推奨される条件につい
て、以下に説明する。
72
同期
4
コード例 5 のスレッド関数には、2 つのクリティカル・セクションが含まれている。2 つの
クリティカル・セクションは異なるデータを保護し、関数 DoFunc1 内の仕事と DoFunc2
内の仕事は互いに独立しているとする。また、各アップデート関数の実行所要時間は、常に
非常に短いとする。2 つのクリティカル・セクションは、DoFunc1 の呼び出しによって区
切られている。DoFunc1 内のスレッドの所要時間が非常に短いときは、2 つのクリティカ
ル・セクションの同期のためのオーバーヘッドを正当化できない。この場合は、コード例 6
のように、2 つの小さなクリティカル・セクションを結合して、1 つの大きなクリティカル・
セクションにする方が良い。しかし、DoFunc1 内の所要時間が、2 つのアップデート・ルー
チンの実行時間の合計よりはるかに長い場合は、この方法は使用できない。これは、クリ
ティカル・セクションのサイズを大きくすると、特にスレッドの数が増えるにつれて、ロッ
クの競合が起こりやすくなるためである。
コード例 5. 異なる共有データのアップデートを保護する 2 つのクリティカル・セクショ
ンを含む、スレッド化された関数
Begin Thread Function ()
Initialize ()
BEGIN CRITICAL SECTION 1
UpdateSharedData1 ()
END CRITICAL SECTION 1
DoFunc1 ()
BEGIN CRITICAL SECTION 2
UpdateSharedData2 ()
END CRITICAL SECTION 2
DoFunc2 ()
End Thread Function ()
コード例 6. この関数によって使用されるすべての共有データのアップデートを保護する 1
つのクリティカル・セクションを含む、スレッド化された関数
Begin Thread Function ()
Initialize ()
BEGIN CRITICAL SECTION 1
UpdateSharedData1 ()
DoFunc1 ()
UpdateSharedData2 ()
END CRITICAL SECTION 1
DoFunc2 ()
End Thread Function ()
73
マルチスレッド・アプリケーションの開発
前の例の変型を考える。この例では、UpdateSharedData2 関数内のスレッドの所要時間
が非常に長いとする。コード例 6 のように、1 つのクリティカル・セクションを使用して
UpdateSharedData1 と UpdateSharedData2 へのアクセスの同期をとる方法は、ロック
の競合が起こりやすくなるため、この場合は推奨できない。実行時に、このクリティカル・
セクションにアクセスしたスレッドは、クリティカル・セクション内で長時間実行を続け
る。その間、他のすべてのスレッドはブロックされる。ロックを保持しているスレッドが
ロックを解放すると、待機中のスレッドのうち 1 つがクリティカル・セクションに入ること
を許可される。他のすべての待機中のスレッドは、長時間ブロックされたままになる。従っ
て、この場合は、コード例 5 のように、2 つのクリティカル・セクションを使用する方が良い。
ロックと特定の共有データを関連付けるのは、推奨のプログラミング手法である。1 つの共
有変数へのすべてのアクセスを同一のロックで保護すれば、他のスレッドは、異なるロック
によって保護された異なる共有変数には自由にアクセスできる。ここで、1 つの共有データ
構造を考える。構造体の各要素に別々のロックを作成することも、1 つのロックを作成して
構造体全体へのアクセスを保護することも可能である。要素のアップデートの演算コストに
よっては、これらの両極端の方法が現実的な解決策になりうる。
ロックの最適な粒度は、通常は両者の中間にある。例えば、共有配列に基づいて、ロックの
ペアを使用できる。1 つのロックは偶数番号の要素を保護し、もう 1 つのロックは奇数番号
の要素を保護する。
UpdateSharedData2 の実行の所要時間が非常に長い場合は、このルーチン内の仕事を分
割し、新しいクリティカル・セクションを作成する方法を推奨する。コード例 7 では、元の
UpdateSharedData2 は、異なるデータを処理する 2 つの関数に分割されている。別々のク
リ テ ィ カ ル・セ ク シ ョ ン を 使 用 す る と、ロ ッ ク の 競 合 の 減 少 が 期 待 さ れ る。
UpdateSharedData2 の実行全体を保護する必要がない場合は、クリティカル・セクショ
ンで関数呼び出しを囲むのではなく、共有データにアクセスするポイントで、関数の中にク
リティカル・セクションの挿入を検討するべきである。
コード例 7. 1 つのクリティカル・セクションを 2 つに分割し、ロックの競合を減らした
コード
Begin Thread Function ()
Initialize ()
BEGIN CRITICAL SECTION 1
UpdateSharedData1 ()
END CRITICAL SECTION 1
DoFunc1 ()
BEGIN CRITICAL SECTION 2
UpdateSharedData2 ()
END CRITICAL SECTION 2
BEGIN CRITICAL SECTION 3
UpdateSharedData3 ()
END CRITICAL SECTION 3
DoFunc2 ()
End Thread Function ()
74
同期
4
推奨事項
クリティカル・セクションのサイズと、ロックの獲得と解放のオーバーヘッドの間でバラン
スをとる。小さなクリティカル・セクションを集めて、ロックのオーバーヘッドを分散する
ことを検討する。ロックの競合が頻繁に発生する大きなクリティカル・セクションを、小さ
なクリティカル・セクションに分割する。ロックの競合が最小限に抑えられるように、ロッ
クと特定の共有データを関連付ける。多くの場合、最適な解決策は、各共有データに異なる
ロックを関連付ける方法と、すべての共有データに 1 つのロックを関連付ける方法の中間に
ある。
同期によって実行がシリアル化されることに注意する。大きなクリティカル・セクションが
ある場合は、アルゴリズムが自然な並行実行性をほとんど持たないか、スレッド間のデータ
分割が最適になっていないことを示す。1 番目の問題については、アルゴリズムを変更する
以外に対処する方法はない。2 番目の問題については、各スレッドが非同期でアクセスでき
る、共有データのローカルコピーを作成してみる。
クリティカル・セクションのサイズとロックの粒度に関する上記の説明は、コンテキスト・
スイッチングのコストを考慮に入れていない。あるスレッドがクリティカル・セクションで
ブロックされてロックの獲得を待機すると、オペレーティング・システムは、そのアイドル
スレッドをアクティブ・スレッドで置き換える。この操作は、コンテキスト・スイッチと呼
ばれる。一般的に、CPU が有益な仕事のために解放されるため、コンテキスト・スイッチ
は望ましい動作である。しかし、小さなクリティカル・セクションに入るのを待機中のス
レッドには、コンテキスト・スイッチより spin-wait の方が効率的である。spin-wait の場合、
待機中のスレッドは CPU を解放しない。従って、spin-wait の使用は、クリティカル・セク
ションの所要時間がコンテキスト・スイッチのコストより小さい場合にのみ推奨できる。
75
マルチスレッド・アプリケーションの開発
コード例 8 は、スレッド関連の Win32* API の使用時に適用される、便利な発見的手法を示
している。この例は、Win32 の CRITICAL_SECTION オブジェクト上で spin-wait オプショ
ンを使用する。クリティカル・セクションに入れないスレッドは、CPU を解放するのでは
なくスピンする。spin-wait の実行中に CRITICAL_SECTION が利用可能になった場合は、コ
ンテキスト・スイッチは避けられる。スピンカウント・パラメータは、スレッドがブロック
されるまでに何回スピンするかを指定する。単一プロセッサ・システム上では、スピンカウ
ント・パラメータは無視される。コード例 8 は、アプリケーション内の各スレッドのスピン
カウントとして 1000、最大スピンカウントとして 8000 を使用している。
コード例 8. 待機中のスレッドの動作を制御する発見的手法
int gNumThreads;
CRITICAL_SECTION gCs;
int main ()
{
int spinCount = 0;
...
spinCount = gNumThreads * 1000;
if (spinCount > 8000) spinCount = 8000;
InitializeCriticalSectionAndSpinCount (&gCs, spinCount);
...
}
DWORD WINAPI ThreadFunc (void *data)
{
...
EnterCriticalSection (&gCs)
...
LeaveCriticalSection (&gCs);
}
使用ガイドライン
ハイパー・スレッディング・テクノロジ対応インテル® プロセッサ上では、コード例 8 で使
用しているスピンカウント・パラメータの値を変更する必要がある。一般的に、spin-wait を
使用すると、ハイパー・スレッディング・テクノロジのパフォーマンスは低下する。複数の
物理 CPU を持つ真の対称型マルチプロセッサ(SMP)とは異なり、ハイパー・スレッディ
ング・テクノロジは、1 つの CPU コア上に 2 つの論理プロセッサを作成する。スピンして
いるスレッドと有益な仕事を実行しているスレッドは、論理プロセッサを求めて競争しなけ
ればならない。従って、スレッドのスピンがマルチスレッド・アプリケーションのパフォー
マンスに与える影響は、SMP システムよりハイパー・スレッディング・システムの方が大
きくなる。コード例 8 の発見的手法では、スピンカウントの値を小さくするか、全く使用し
ない方が良い。
76
同期
4
参考資料
本シリーズの参照個所 :
インテル® ソフトウェア開発ツール、2.4「インテル® スレッド・チェッカーに
よるマルチスレッド・エラーの検出」
インテル® ソフトウェア開発ツール、2.5「スレッド・プロファイラによる
OpenMP パフォーマンスの評価」
メモリ管理、5.2「スレッドごとのローカル・ストレージによる同期の削減」
77
マルチスレッド・アプリケーションの開発
4.2 手作業でコーディングした同期ルーチンではなく、
スレッド関連の API が提供する同期ルーチンを使用する
カテゴリ
同期
記述範囲
一般的なマルチスレッド処理
キーワード
同期、spin-wait、ハイパー・スレッディング、Win32* スレッド、OpenMP*、Pthreads
摘要
アプリケーション・プログラマは、スレッド関連の API が提供する構文を使用するのではな
く、手作業でコーディングした同期ルーチンを記述して、同期のためのオーバーヘッドを軽
減したり、既存の構文の機能とは異なる機能を実行しようとするときがある。残念なこと
に、手作業でコーディングした同期ルーチンを使用すると、マルチスレッド・アプリケー
ションのパフォーマンス、パフォーマンス・チューニング、またはデバッグに悪影響を与え
ることがある。
背景情報
スレッド関連の API が提供する同期ルーチンに関連するオーバーヘッドを避けるために、手
作業でコーディングした同期ルーチンを記述したくなる場合がある。プログラマが独自の同
期ルーチンを記述するもう 1 つの理由は、スレッド関連の API の同期ルーチンの機能が、希
望する機能と完全には一致しないことである。しかし、残念ながら、スレッド関連の API
ルーチンと比較して、手作業でコーディングした同期ルーチンには重大な欠点がある。
手作業でコーディングした同期ルーチンの 1 つの欠点は、異なるハードウェア・アーキテク
チャおよびオペレーティング・システム上では高性能を保証できない点である。以下の C で
記述された手作業でコーディングしたスピンロックの例は、この問題の理解に役立つ。
#include <ia64intrin.h>
void acquire_lock( int *lock )
{
while
(_InterlockedCompareExchange (lock, TRUE, FALSE) ==TRUE );
}
void release_lock (int *lock)
{
*lock = FALSE;
}
78
同期
4
_InterlockedCompareExchange コンパイラ組み込み関数は、この組み込み関数の実行
中は他のスレッドは指定されたメモリ上の位置を修正できないことを保証する、インター
ロック・メモリ操作である。この組み込み関数は、最初に、1 番目の引数内のアドレスのメ
モリ内容と 3 番目の引数の値を比較し、一致した場合は、1 番目の引数で指定されるメモリ
アドレスに 2 番目の引数の値を格納する。指定されたアドレスのメモリ内容内で見つかった
元の値は、組み込み関数によって返される。この例では、acquire_lock ルーチンは、メ
モリ上の位置 lock の内容がアンロック状態(FALSE)になるまでスピンする。その時点で、
(lock の内容を TRUE に設定することで)ロックが獲得され、このルーチンはリターンする。
release_lock ルーチンは、メモリ上の位置 lock の内容を FALSE に戻し、ロックを解放
する。
このロックのコーディングは、一見すると簡単で十分に効率的に思われるが、いくつかの問
題を含んでいる。第 1 に、多数のスレッドが同じメモリ上の位置でスピンしているとき、
ロックが解放された時点で、キャッシュの無効化と大量のメモリ・トラフィックが発生する
場合がある。このため、スレッドの数が増えるほど、スケーラビリティが低下する。第 2 に、
このコードが使用しているアトミックなメモリ・プリミティブは、すべてのプロセッサ・
アーキテクチャ上で利用できるわけではないため、移植性が制限される。第 3 に、ハイパー・
スレッディング・テクノロジなどの特定のプロセッサ・アーキテクチャ機能では、スピン
ループを多用するとパフォーマンスが低下する。第 4 に、while ループは、オペレーティ
ング・システムから見ると、有益な演算を実行しているように見えるため、オペレーティン
グ・システムのスケジューリングの公平性に悪影響を与える。これらの問題にはすべて解決
法が存在するが、それらを使用すると、コードが非常に複雑になり、エラーの有無の検証が
難しくなる。また、移植性を維持しながらコードをチューニングすることも困難である。こ
れらの課題は、スレッド関連の API の開発者に任せた方が良い。API の同期構文は、移植性
とスケーラビリティについて、長時間にわたる検証とチューニングが行われている。
手作業でコーディングした同期ルーチンのもう 1 つの重大な欠点は、マルチスレッド環境向
けのプログラミング・ツールの精度を低下させることである。例えば、インテル® スレッド
化ツールは、スレッド化されたアプリケーション・プログラムのパフォーマンスとエラーの
有無に関する正確な情報を提供するために、同期構文を特定する必要がある(パフォーマン
スの評価については、インテル・ソフトウェア開発ツール、2.5「スレッド・プロファイラ
による OpenMP パフォーマンスの評価」を参照。エラーの検出については、インテル・ソフ
トウェア開発ツール、2.4「インテル® スレッド・チェッカーによるマルチスレッド・エラー
の検出」を参照)。スレッド化ツールは、多くの場合、サポートしているスレッド関連の API
が提供する同期構文の機能を特定し、特性評価する。上記の例のように、標準的な同期 API
を使ってコーディングされていない同期は、スレッド化ツールが特定し、理解することが困
難である。スレッド化ツールは、手作業でコーディングした同期ルーチンを特定して評価す
るために、プログラマがツール固有のディレクティブ、プラグマ、または API コールの形式
で指定するヒントをサポートしていることもある。特定のツールがプログラマのヒントをサ
ポートしている場合でも、このようなヒントを使用すると、スレッド関連の API の同期ルー
チンを使用したときと比べて、アプリケーション・プログラムの分析の精度が低下する。性
能上の問題の原因を検出しにくくなったり、スレッド化エラー検出ツールが、実際には存在
しないレース状態や同期構文の欠落をレポートする場合がある。
79
マルチスレッド・アプリケーションの開発
推奨事項
できるだけ、手作業でコーディングした同期ルーチンの使用を避ける。代わりに、使用するスレッド関連の
API が提供するルーチンを使用する。これには、OpenMP の omp_set_lock/omp_unset_lock または
critical/end critical ディレクティブ、Pthreads のpthread_mutex_lock/pthread_mutex_unlock、
Win32 API の EnterCriticalSection/LeaveCriticalSection または WaitForSingleObject(また
は WaitForMultipleObjects)と ReleaseMutex などがある。スレッド関連の API の同期ルーチンと構
文について検討し、アプリケーションに最適なものを選ぶことを推奨する。
スレッド関連の API の同期構文では必要な機能が得られない場合は、プログラムに異なるア
ルゴリズムを使用して、同期の削減または変更を検討する。さらに、経験豊富なプログラマ
は、独自の同期構文を最初から作成するのではなく、API の簡単な同期構文に基づいて独自
の同期構文を作成できる。性能上の理由で、手作業でコーディングした同期構文を使用しな
ければならない場合は、手作業でコーディングした同期構文を、スレッド関連の API が提供
する機能的に同等の同期構文で簡単に置き換えられるように、前処理ディレクティブの使用
を検討する。これによって、スレッド化ツールの精度が向上する。
使用ガイドライン
API の簡単な同期構文に基づいて独自の同期構文を作成する場合は、共有されるメモリ上の
位置でのスピンループの使用を避けると、パフォーマンスのスケーラビリティを維持でき
る。また、コードを移植する必要がある場合は、アトミックなメモリ・プリミティブの使用
も避けた方が良い。ただし、スレッド化パフォーマンス解析 / エラー検出ツールは、独自の
同期構文の基となる単純な同期構文を正しく特定できても、独自の同期構文の機能を推論で
きないときがある。この場合、ツールの精度は低下する。
参考資料
本シリーズの参照個所 :
インテル® ソフトウェア開発ツール、2.4「インテル® スレッド・チェッカーに
よるマルチスレッド・エラーの検出」
インテル® ソフトウェア開発ツール、2.5「スレッド・プロファイラによる
OpenMP パフォーマンスの評価」
本章、4.3「同期用の Win32 アトミック、ユーザ空間ロック、カーネル・オブ
ジェクトの対比」
本章、4.4「できるだけノンブロッキング・ロックを使用する」
他の参考資料 :
John Mellor-Crummey 著『Algorithms for Scalable Synchronization on
Shared-Memory Multiprocessors』、ACM Transactions on Computer Systems、9 号、
21 ~ 65 ページ、1991 年
『インテル® Pentium® 4 プロセッサおよびインテル® Xeon™ プロセッサ最適化リ
ファレンス・マニュアル』
、第 7 章「マルチスレッドおよびハイパー・スレッ
ディング・テクノロジ」、インテル® デベロッパ・サービス
80
同期
4
4.3 同期用の Win32* アトミック、ユーザ空間ロック、カーネル・
オブジェクトの対比
カテゴリ
同期
記述範囲
Win32* マルチスレッド処理
キーワード
同期、ロックの競合、システム・オーバーヘッド、排他制御、Win32 スレッド
摘要
各スレッドは、同期ポイントで待機している間、有益な仕事をしていない。残念なことに、
マルチスレッド・プログラムには、通常はある程度の同期が必要である。Win32 API は、異
なる有用性とシステム・オーバーヘッドを持つ複数の同期機構を提供する。
背景情報
その性質上、同期構文は、実行をシリアル化する。しかし、全く同期を必要としないマルチ
スレッド・プログラムはほとんど存在しない。幸いにも、適切な構文を選ぶことにより、同
期に関連するシステム・オーバーヘッドは多少軽減される。ここでは、インクリメント文
(例えば、var++)を使用して、各種の構文の例を示す。更新される変数がスレッド間で共
有されている場合、load → write → store 命令はアトミックでなければならない(すなわち、
一連の命令が完了する前にプリエンプションがあってはならない)。Win32 API は、原子性
(atomicity)を保証するための機構をいくつか備えている。以下のコードは、3 種類の機構を
示している。
#include <windows.h>
CRITICAL_SECTION cs;
/* Initialized in main() */
HANDLE mtx;
/* CreateMutex called in main() */
static LONG counter= 0;
void IncrementCounter ()
{
// Synchronize with Win32 interlocked function
InterlockedIncrement (&counter);
// Synchronize with Win32 critical section
EnterCriticalSection (&cs);
counter++;
LeaveCriticalSection (&cs);
// Synchronize with Win32 mutex
WaitForSingleObject (mtx, INFINITE);
counter++
ReleaseMutex (mtx);
}
81
マルチスレッド・アプリケーションの開発
各構文の利点と欠点について以下に説明する。
Win32 インターロック関数(InterlockedIncrement、InterlockedDecrement、
InterlockedExchange、InterlockedExchangeAdd、InterlockedCompareExchange)
は、簡単な操作に限定されるが、クリティカル・リージョンより高速である。また、必要な関
数呼び出しの回数も少ない。Win32 クリティカル・リージョンの処理の開始と終了には、
EnterCriticalSection と LeaveCriticalSection か、WaitForSingleObject と
ReleaseMutex の呼び出しが必要である。インターロック関数はノンブロッキング型であるが、
EnterCriticalSection と WaitForSingleObject(または WaitForMultipleObjects)は、
同期オブジェクトが利用できない場合、スレッドをブロックする。
クリティカル・リージョンが必要な場合、Win32 CRITICAL_SECTION で同期をとる方が、Win32
mutex、セマフォ、イベント HANDLE で同期をとるより、システム・オーバーヘッドははるかに
小さくなる。これは、Win32 CRITICAL_SECTION はユーザ空間オブジェクトであり、Win32
mutex、セマフォ、イベント HANDLE はカーネル空間オブジェクトだからである。Win32 クリ
ティカル・セクションは、通常は Win32 mutex より高速で実行されるが、汎用性は mutex より
低い。mutex は、他のカーネル・オブジェクトのように、プロセス間の同期に使用できる。
WaitForSingleObject および WaitForMultipleObjects 関数を使用して、timed-wait も可
能である。スレッドは、mutex を獲得するまで無制限に待機するのではなく、指定した制限時
間が経過した後に実行を再開する。wait-time を 0 に設定すると、各スレッドは mutex を利用で
きるかどうかをブロッキングなしでテストできる(TryEnterCriticalSection 関数を使用
して、CRITICAL_SECTION を利用できるかどうかをブロッキングなしでチェックするのも可
能であることに注意)
。最後に、mutex を保持しているスレッドが mutex を解放せずに終了した
場合、オペレーティング・システムは、待機中のシステムがデッドロックにならないように、
ハンドルに信号を送る。CRITICAL_SECTION を保持しているスレッドが CRITICAL_SECTION
を解放せずに終了した場合は、この CRITICAL_SECTION に入るのを待機しているスレッドは
デッドロックになる。
Win32 スレッドは、すでに他のスレッドが保持している CRITICAL_SECTION または mutex
HANDLE の獲得を試みた場合、直ちに CPU をオペレーティング・システムに解放する。一般
的に、これは望ましい動作である。スレッドはブロックされ、CPU は有益な仕事を自由に行
える。しかし、スレッドのブロッキングとアンブロッキングにはコストがかかる。スレッドが
ブロックされる前に、もう一度ロックの獲得を試みる方が良い場合もある(例えば、SMP シ
ステム上で、小さなクリティカル・セクションの場合)
。Win32 CRITICAL_SECTION は、ユー
ザが設定できるスピンカウントによって、スレッドが CPU を解放するまでの時間を制御する。
InitializeCriticalSectionAndSpinCount および SetCriticalSectionSpinCount
関数は、特定の CRITICAL_SECTION に入ろうとするスレッドのスピンカウントを設定する。
82
同期
4
推奨事項
変数の簡単な操作(すなわち、インクリメント、デクリメント、交換)には、高速でオー
バーヘッドが小さい Win32 インターロック関数を使用する。
プロセス間の同期または timed-wait が必要な場合は、Win32 mutex、セマフォ、またはイベ
ント HANDLE を使用する。それ以外の場合は、システム・オーバーヘッドが小さい Win32
CRITICAL_SECTION を使用する。
InitializeCriticalSectionAndSpinCount および SetCriticalSectionSpinCount
関数を使用して、Win32 CRITICAL_SECTION のスピンカウントを制御する。競合が発生しや
すい小さいクリティカル・セクションの場合、待機中のスレッドが CPU を解放するまでのス
ピン時間を制御することは、特に重要である。SMP システムおよびハイパー・スレッディン
グ・テクノロジ対応 CPU 上では、スピンカウントはパフォーマンスに大きな影響を与える。
使用ガイドライン
Win32 インターロック関数を連続して呼び出す場合は、スレッドのプリエンプションに注意
する。例えば、
図14 の2つのコード・セグメントを複数のスレッドで実行した場合、
localVar
に常に同じ値が得られるとは限らない。インターロック関数を使用する例では、任意の関数
呼び出しの間にスレッドのプリエンプションがあると、予想不可能な結果が生じる。クリ
ティカル・セクションの例は、両方のアトミック操作(すなわち、グローバル変数 N の更新
と localVar への代入)が保護されているため、安全である。
83
マルチスレッド・アプリケーションの開発
図 14.インターロック関数とクリティカル・セクションの基本的な違い
static LONG N = 0;
static LONG N = 0;
LONG localVar;
LONG localVar;
InterlockedIncrement (&N);
EnterCriticalSection
InterlockedIncrement (&N);
(&lock);
InterlockedExchange
localVar = (N += 2);
(&localVar, N);
LeaveCriticalSection
(&lock);
安全のために、Win32 クリティカル・リージョンは(CRITICAL_SECTION 変数または mutex
HANDLE のいずれを使用した場合も)、入口点と出口点を 1 つだけにするべきである。クリ
ティカル・セクションの中にジャンプすると、同期に失敗する。LeaveCriticalSection
または ReleaseMutex を呼び出さずにクリティカル・セクションから外にジャンプすると、
待機中のスレッドがデッドロックになる。また、単一の入口点と出口点を使用すると、コー
ドがわかりやすくなる。
CRITICAL_SECTION 変数を保持しているスレッドがこれらの変数を解放せずに終了する
と、待機中のスレッドがデッドロックになるため、このような状況を防ぐ。
参考資料
本シリーズの参照個所 :
インテル® ソフトウェア開発製品、2.3「VTune™ パフォーマンス・アナライザ
によるスレッド間のフォルス・シェアリングの回避と特定」
本章、4.2「手作業でコーディングした同期ルーチンではなく、スレッド関連
の API が提供する同期ルーチンを使用する」
本章、4.4「できるだけノンブロッキング・ロックを使用する」
他の参考資料 :
Johnson M. Hart 著『Win32 System Programming(2nd Edition)』、Addison-Wesley
刊、2001 年
Jim Beveridge、Robert Wiener 共著『Multithreading Applications in Win32』、
Addison-Wesley 刊、1997 年
84
同期
4
4.4 できるだけノンブロッキング・ロックを使用する
カテゴリ
同期
記述範囲
Windows* スレッド、Pthreads、IA-32、インテル® Itanium® プロセッサ
キーワード
ノンブロッキング・ロック、同期、クリティカル・セクション、コンテキスト・スイッチ、
spin-wait
摘要
スレッドは、サポートしているスレッド化ライブラリが提供する同期プリミティブを実行す
れば、共有リソース上で同期をとる。これらのプリミティブ(mutex、セマフォなど)によっ
て、1 つのスレッドがロックを所有している間、他のスレッドはタイムアウト機構に基づい
てスピンするかまたはブロックされる。スレッドをブロックすると、コンテキスト・スイッ
チのコストが生じる。スレッドをスピンさせると、
(非常に短時間でない限り)CPU の実行
リソースが浪費される。それに対して、ノンブロッキング・システム・コールを使用する
と、競合するスレッドがロックに失敗したときは元の処理に戻って有益な仕事を実行できる
ため、実行リソースの浪費が避けられる。
背景情報
ほとんどのスレッド化ライブラリ(Win32* および POSIX スレッド API など)には、ブロッ
キング型とノンブロッキング型のスレッド同期プリミティブが含まれている。多くの場合、
デフォルトではブロッキング・プリミティブが使用される。ロックに成功すると、そのス
レッドがロックの制御を取得し、クリティカル・セクション内のコードを実行する。しか
し、ロックに失敗した場合は、コンテキスト・スイッチが発生し、そのスレッドは待機中の
スレッドのキューに入れられる。コンテキスト・スイッチにはコストがかかるため、以下の
理由でできるだけ避けるべきである。
•
コンテキスト・スイッチのオーバーヘッドは、特にスレッドがカーネルスレッドに基づ
いて実装されている場合、かなり大きくなる。
•
スレッドがロックの制御を取得するまで、同期コールに続くアプリケーション内の有益
な仕事を実行できない。
ノンブロッキング・システム・コールを使用すると、性能上のペナルティを軽減できる。こ
の場合、クリティカル・セクションのロックに失敗したアプリケーション・スレッドは、直
ちに実行を再開する。これによって、コンテキスト・スイッチのオーバーヘッドと、ロック
によるスピンが避けられる。スレッドは、次にロック制御の取得を試みるまで、有益な仕事
を実行できる。
85
マルチスレッド・アプリケーションの開発
推奨事項
ノンブロッキング同期関数を使用して、コンテキスト・スイッチのオーバーヘッドを避け
る。ノンブロッキング同期関数は、通常は 'try' で始まる。例えば、Win32 API は、次のよう
なブロッキング型およびノンブロッキング型のクリティカル・セクションを提供する。
void EnterCriticalSection (LPCRITICAL_SECTION cs);
bool TryEnterCriticalSection (LPCRITICAL_SECTION cs);
クリティカル・セクションの所有権の取得に成功した場合、TryEnterCriticalSection
は、ブール値 TRUE を返す。失敗した場合、この関数は FALSE を返し、スレッドは実行を
再開する。
以下の例は、ノンブロッキング同期関数の一般的な使用例を示している。
CRITICAL_SECTION cs;
void threadfoo()
{
while (TryEnterCriticalSection (&cs) == FALSE)
{
// Useful work
}
//
// Code requiring protection by critical section
//
LeaveCriticalSection (&cs);
}
同様に、Pthreads は、次のようなノンブロッキング版の mutex 関数を提供する。
int pthread_mutex_lock (pthread_mutex_t *mutex);
int pthread_mutex_trylock (pthread_mutex_t *mutex);
Win32 同期プリミティブのタイムアウトを設定することも可能である。Win32 API の
WaitForSingleObject および WaitForMultipleObjects 関数は、カーネル・オブジェ
クト(すなわち、HANDLE)上で同期をとる。
DWORD WaitForSingleObject (HANDLE hHandle, DWORD dwMilliseconds);
ここで、hHandle はカーネル・オブジェクトのハンドルである。
dwMilliseconds は、カーネル・オブジェクトの信号が送られない場合に、関数がリター
ンするまでのタイムアウト間隔である。この値を INFINITE に指定すると、スレッドは無制
限に待機する。スレッドは、関連するカーネル・オブジェクトの信号が送られるか、ユーザ
が指定した時間間隔が経過するまで待機する。この時間間隔が経過すると、スレッドは実行
を再開する。以下の例は、ノンブロッキング同期のための WaitForSingleObject の使用
例を示している。
86
同期
4
void threadfoo ()
{
DWORD ret_value;
HANDLE hHandle;
ret_value = WaitForSingleObject (hHandle, 0);
if (ret_value == WAIT_TIME_OUT)
{
// Thread could not acquire lock within the time interval
//
// Other useful work
//
}
else if (ret_value == WAIT_OBJECT_0)
{
// Thread acquired lock within the time interval
//
// Code requiring protection by critical section
//
}
}
同様に、WaitForMultipleObjects を使用すると、スレッドは複数のカーネル・オブジェ
クトのシグナル状態を待機する。
使用ガイドライン
ノンブロッキング同期関数(例えば、TryEnterCriticalSection)を使用する場合は、
共有オブジェクトを解放する前に、戻り値を確認して要求が成功したかどうかをチェックす
る。
参考資料
本シリーズの参照個所 :
インテル® ソフトウェア開発製品、2.3「VTune™ パフォーマンス・アナライザ
によるスレッド間のフォルス・シェアリングの回避と特定」
本章、4.2「手作業でコーディングした同期ルーチンではなく、スレッド関連
の API が提供する同期ルーチンを使用する」
本章、4.3「同期用の Win32 アトミック、ユーザ空間ロック、カーネル・オブ
ジェクトの対比」
本章、4.4「できるだけノンブロッキング・ロックを使用する」
本章、4.5「ダブルチェック・パターンを使用して、1 回限りのイベントでの
ロック獲得を避ける」
他の参考資料 :
Aaron Cohen、Mike Woodring 共著『Win32 Multithreaded Programming』、O'Reilly
and Associates 刊、1998 年
Jim Beveridge、Robert Wiener 共著『Multithreading Applications in Win32 - the
Complete Guide to Threads』
、Addison Wesley 刊、1997 年
Bil Lewis、Daniel J Berg 共著『Multithreaded Programming with Pthreads』、Sun
Microsystems Press 刊、1998 年
87
マルチスレッド・アプリケーションの開発
4.5 ダブルチェック・パターンを使用して、1 回限りのイベントで
のロック獲得を避ける
カテゴリ
同期
記述範囲
一般的なマルチスレッド処理
キーワード
ロックの競合、同期、排他制御、Win32* スレッド、Pthreads
摘要
ロックの獲得は、同期と同じように、コストのかかる操作である。1 回限りのイベント(例
えば、初期化、ファイルのオープン / クローズ、動的メモリ割り当て)では、多くの場合、
ダブルチェック・ロッキング(DCL)を使用して、不要なロックの獲得を避けられる。
背景情報
同期(この場合はロックの獲得)には、オペレーティング・システムとの 2 回の相互作用
(すなわち、ロックとアンロック)が必要である。これによって大きなオーバーヘッドが生
じる。例えば、グローバルな読み取り専用テーブルを初期化する場合、すべてのスレッドが
この操作を実行する必要はないが、初期化が行われたかどうかはすべてのスレッドがチェッ
クしなければならない。一度だけ実行される操作(例えば、初期化、ファイルのオープン /
クローズ、動的メモリ割り当て)では、多くの場合、DCL を使用して、不要なロックの獲
得を避けられる。以下の疑似コードに示すように、DCL 内で if テストを使用して、最初の
初期化後のロックを避けられる。
Boolean initialized = FALSE
function InitOnce
{
if not initialized
{
acquire lock
if not initialized ← Double-check!
{
perform initialization
initialized = TRUE
}
release lock
}
}
88
同期
4
この疑似コードには、興味深い点がいくつかある。第 1 に、複数のスレッドが最初の if テス
トを真として評価する可能性があるが、最初にロックを獲得したスレッドだけが初期化を実
行でき、ブール変数を真に設定できる。ロックが解放されると、これ以降のスレッドは、
ブール変数を再チェックする。ブール制御変数のダブルチェックを行わないと、異なるデー
タでテーブルが再初期化され、予想不可能な結果が生じる可能性がある。第 2 に、初期化の
実行後にこの関数を呼び出したスレッドは、ロックを獲得しない。最初の if テストは偽と評
価される。第 3 に、初期化が完了するまで、どのスレッドも元の処理に戻れない。
最後に、ブール変数についてデータレースが存在する。厳密には、あるスレッドが変数の値
を変更している間に、他のスレッドがその値を読み取る可能性がある。ロックを保持してい
るスレッド以外はこの変数を変更できないため、このデータレースは無害である。しかし、
この場合でも、インテル® スレッド・チェッカーは、ブール変数上のストレージの競合をレ
ポートする(2.4「インテル ® スレッド・チェッカーによるマルチスレッド・エラーの検出」
を参照)。
推奨事項
1 回限りの操作を実行するときは、DCL を使用して、ロックの獲得の繰り返しを避ける。操
作が完了したかどうかを各スレッドが繰り返しチェックする場合、DCL は特に便利である。
以下のソースコードは、C と Win32 API を使用した DCL のコーディングを示している。
#include <windows.h>
CRITICAL_SECTION lock; /* Initialized elsewhere */
static int initialized = 0;
void init_once ()
{
if (!initialized)
{
EnterCriticalSection (&lock);
if (!initialized)
{
/* Perform initialization */
initialized = 1;
}
LeaveCriticalSection (&lock);
}
}
89
マルチスレッド・アプリケーションの開発
以下のソースコードは、C と Pthreads を使用した DCL のコーディングを示している。
#include <pthread.h>
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
static int initialized = 0;
void init_once ()
{
if (!initialized)
{
pthread_mutex_lock (&lock);
if (!initialized)
{
/* Perform initialization */
initialized = 1;
}
pthread_mutex_unlock (&lock);
}
}
念のために、OpenMP* を使用した DCL の例を以下に示す。
subroutine init_once
logical, save :: init = .FALSE.
if (.not. init) then
!$omp critical (once)
if (.not. init) then
! Perform initialization
init = .TRUE.
endif
!$omp end critical
endif
end subroutine init_once
OpenMP には DCL の機能を表現するプラグマが含まれているため(すなわち、single ワー
クシェアリング構文または master/barrier の組み合わせ)、通常は OpenMP プログラム
に DCL は不要である。
使用ガイドライン
共有読み取り専用データを初期化する場合、複数のスレッドに初期化を非同期で実行させた
くなる。すべてのスレッドがグローバル・データに同じ値を書き込む場合は、初期化は正常
に行われる。しかし、初期化を非同期で実行すると、複数のスレッドが互いのキャッシュ・
ラインを無効化するため、大きな性能上のペナルティが生じる。
DCL と同じ状況で pthread_once 関数も使用できるが、この関数の方がシステム・オー
バーヘッドが大きくなる。
一部の Java* 仮想マシンは、Java メモリモデルを誤った方法で実装しているため、Java で
DCL を使用する際は注意する必要がある。
90
同期
4
参考資料
本シリーズの参照個所 :
インテル® ソフトウェア開発製品、2.4「インテル® スレッド・チェッカーによ
るマルチスレッド・エラーの検出」
本章、4.3「同期用の Win32 アトミック、ユーザ空間ロック、カーネル・オブ
ジェクトの対比」
本章、4.4「できるだけノンブロッキング・ロックを使用する」
他の参考資料 :
Douglas C. Schmidt、Tim Harrison 共著「Double-Checked Locking」、Robert
Martin、Frank Buschmann、Dirke Riehle 編『Pattern Languages of Program Design
3』、Addison-Wesley 刊、1997 年
Brian Goetz 著「Double-check locking: Clever, but broken」、JavaWorld、2001 年 2
月号
91
マルチスレッド・アプリケーションの開発
92
5
メモリ管理
アプリケーションに並行実行性を追加すると、明白な方法でパフォーマンスを向上させるこ
とができる。本シリーズの他の章では、スレッド化されたアプリケーションのパフォーマン
スに影響を与える多くの問題について説明している。これらの手法ほど明白ではないが、ス
レッド化されたアプリケーションのパフォーマンスに影響を与える重要な考慮事項として、
ヒープリソースの競合を避けること、共有ストレージの代わりにスレッドごとのローカルな
ストレージを使用して同期を減らすこと、メモリ割り当てを注意深く管理することなどがあ
る。本章では、これらのメモリ管理の問題について説明する。
93
マルチスレッド・アプリケーションの開発
5.1 スレッド間のヒープの競合の回避
カテゴリ
メモリ管理
記述範囲
一般的なマルチスレッド処理
キーワード
ヒープの競合、同期、動的メモリ割り当て、ロックの競合、スタックの割り当て
摘要
システムヒープからのメモリの割り当ては、コストのかかる操作である。割り当てをスレッ
ドセーフにするために、ロックを使用して、ヒープへのアクセスを同期する。このロックの
競合があると、マルチスレッド化による性能上のメリットが制限される。この問題を解決す
るには、割り当て方法を変更して、共有ロックの使用を避ける。
背景情報
システムヒープ(malloc によって使用される)は、共有リソースである。複数のスレッド
がヒープを使用する場合の安全性を保つには、共有ヒープへのアクセスを制限する同期を追
加する必要がある。同期(この場合はロックの獲得)には、オペレーティング・システムと
の 2 回の相互作用(すなわち、ロックとアンロック)が必要である。これによって大きな
オーバーヘッドが生じる。
インテル ® コンパイラ 7.0 以降の OpenMP* ライブラリは、2 つの関数 kmp_malloc と
kmp_free をエクスポートする。これらの関数は、OpenMP チームの各スレッドに付加され
る、スレッドごとのヒープを保守する。これらの関数を呼び出したスレッドは、標準システ
ムヒープへのアクセスを保護するロックの使用を避けられる。また、threadprivate ディ
レクティブを使用して、OpenMP チームの各スレッド用に、グローバルに宣言された変数の
プライベート・コピーを作成できる。
Win32* HeapCreate 関数を使用して、アプリケーションが使用するすべてのスレッドに、
別々のヒープを割り当てられる。各ヒープにアクセスするスレッドは 1 つだけであるため、
フラグ HEAP_NO_SERIALIZE を使用して、この新しいヒープ上の同期の使用を無効にでき
る。
ヒープハンドルが TLS(Thread Local Storage)の位置に格納されている場合は、アプリケーショ
ン・スレッドがメモリの割り当てまたは解放を必要とするときは、いつでもこのヒープを使用
できる。ただし、この方法で割り当てられたメモリは、割り当てを実行したスレッドが明示的
に解放しなければならない。Pthreads アプリケーションでは、pthread_key_create および
pthread_{get|set}specific API を使用して TLS にアクセスできるが、このグローバ
ル・ストレージの管理はプログラマの責任となる。
94
メモリ管理
5
メモリを割り当てたスレッドとそのメモリを解放するスレッドが一致するとは限らない場
合は、より一般的な方法を使用する必要がある。この場合は、参考資料の項目に示した市販
のヒープ管理ツールの使用を推奨する。
以下の例では、Win32 API のいくつかの機能を使用している。
#include <windows.h>
static DWORD tls_key;
__declspec (dllexport) void* thr_malloc (size_t n)
{
return HeapAlloc (TlsGetValue (tls_key), 0, n);
}
__declspec (dllexport) void thr_free (void *ptr)
{
HeapFree (TlsGetValue (tls_key), 0, ptr);
}
BOOL WINAPI DllMain
(HINSTANCE hinstDLL,
DWORD fdwReason,
LPVOID lpReserved)
{
switch (fdwReason)
{
case DLL_PROCESS_ATTACH:
// Use Thread Local Storage to remember the heap
tls_key = TlsAlloc ();
TlsSetValue (tls_key, GetProcessHeap ());
break;
case DLL_THREAD_ATTACH:
// Use HEAP_NO_SERIALIZE to avoid lock contention
TlsSetValue
(tls_key, HeapCreate (HEAP_NO_SERIALIZE, 0, 0));
break;
case DLL_THREAD_DETACH:
HeapDestroy (TlsGetValue (tls_key));
break;
case DLL_PROCESS_DETACH:
TlsFree (tls_key);
break;
}
return TRUE; // Successful DLL_PROCESS_ATTACH
}
第 1 に、このコードは、ダイナミック・ロード・ライブラリ(DLL)を使用して、スレッド
が作成された時点でスレッドを登録している。また、TLS を使用して、各スレッドに割り当
てられたヒープを記憶している。最後に、Win32 API の機能を使用して、非同期ヒープを互
いに独立して管理している。
95
マルチスレッド・アプリケーションの開発
推奨事項
互いに独立した複数のヒープを使用する方法以外に、他の手法を組み込んで、システムヒー
プの保護に使用される共有ロックから発生するロックの競合も最小限に抑えられる。メモリ
へのアクセスが、小さな字句解析コンテキストの範囲内に限られる場合は、alloca ルーチ
ンを使用して、現在のスタックフレームからメモリを割り当てられる。このメモリは、関数
のリターン時に自動的に解放される。
もう 1 つの手法は、スレッドごとの空きリストである。最初は、malloc を使用してシステ
ムヒープからメモリを割り当てる。通常ならメモリを解放するはずの時点で、スレッドごと
のリンク済みリストにメモリを追加する。スレッドが同じサイズのメモリを再び割り当てる
場合は、システムヒープに戻らずに、格納されている割り当てをこのリストから直ちに取り
出せる。
struct MyObject
{
struct MyObject *next;
};
static __declspec(thread) struct MyObject *freelist_MyObject = 0;
struct MyObject *malloc_MyObject ()
{
struct MyObject *p = freelist_MyObject;
if (p == 0)
return malloc (sizeof (struct MyObject));
freelist_MyObject = p->next;
return p;
}
void free_MyObject (struct MyObject *p)
{
p->next = freelist_MyObject;
freelist_MyObject = p;
}
使用ガイドライン
すべての最適化にはトレードオフが伴う。この場合は、システムヒープ上の競合を削減する
代償として、メモリ利用率が上昇する。各スレッドが独自のプライベート・ヒープまたはオ
ブジェクトの集合体を保守しているときは、他のスレッドはこれらの領域を利用できない。
その結果、スレッド間に「メモリの不均衡」が発生する場合がある。この状態は、各スレッ
ドが実行する仕事の量が異なる場合に発生する「負荷の不均衡」によく似ている(アプリ
ケーションのスレッド化、3.3「ロード・バランスと並列性能」を参照)
。メモリの不均衡が
あると、ワーキング・セットのサイズが増大し、アプリケーションの合計メモリ利用率も上
昇する。メモリ利用率の上昇は、通常はパフォーマンスにわずかな影響しか与えない。しか
し、メモリ利用率の上昇によって、利用可能メモリが使い果たされたときは例外である。こ
の場合は、アプリケーションの停止やディスクへのスワップが発生する。
96
メモリ管理
5
参考資料
本シリーズの参照個所 :
インテル® ソフトウェア開発製品、2.3「VTune™ パフォーマンス・アナライザ
によるスレッド間のフォルス・シェアリングの回避と特定」
インテル® ソフトウェア開発製品、2.4「インテル® スレッド・チェッカーによ
るマルチスレッド・エラーの検出」
同期、4.1「ロックの競合と大小のクリティカル・セクションの管理」
他の参考資料 :
MicroQuill SmartHeap for SMP
HOARD Memory Allocator
以下の Win32 関数に関する解説書
HeapAlloc、HeapCreate、HeapFree
TlsAlloc、TlsGetValue、TlsSetValue
Alloca
97
マルチスレッド・アプリケーションの開発
5.2 スレッドごとのローカル・ストレージによる同期の削減
カテゴリ
メモリ管理
記述範囲
一般的なマルチスレッド処理
キーワード
スレッドごとのローカル・ストレージ、同期、OpenMP*、Pthreads、Win32* スレッド
摘要
同期は、多くの場合、マルチスレッド・プログラムのパフォーマンスを制限する、コストの
かかる操作である。条件によっては、複数のスレッドが共有するデータ構造の代わりに、ス
レッドごとのローカルデータ構造を使用すれば、同期を削減し、プログラムの実行を高速化
できる。
背景情報
スレッドのグループによってデータ構造が共有され、少なくとも 1 つのスレッドがデータ構
造に書き込む場合は、すべてのスレッドから認識される共有データの整合性が常に保証され
るように、スレッド間の同期をとる必要がある。この状況で各スレッドのアクセスを同期す
る一般的な方法は、1 つのスレッドがロックを獲得し、共有データ構造の読み込みまたは書
き込みを実行した後、ロックを解放する方法である。
すべての形式のロック操作で、ロックデータ構造の保守のためのオーバーヘッドが生じる。
これらのロック操作は、アトミックな命令を使用するため、最新のプロセッサの実行速度を
低下させる。また、同期されたコードの中では並列実行が行えないため、逐次実行のボトル
ネックが形成され、同期それ自体がプログラムの実行速度を低下させる。従って、遅れが許
されないコード・セクション内で同期を行うと、コードのパフォーマンスが低下する。
プログラムを記述し直して、共有データ構造の代わりにスレッドごとのローカル・ストレー
ジを使用できる場合は、遅れが許されないマルチスレッド・コード・セクションから同期を
削除できる。コードの性質上、共有データへのアクセスのリアルタイムの順序づけが重要で
ない場合は、この方法を使用できる。また、アクセスの順序づけが重要であっても、順序づ
けを安全に延期して、頻度の低い、タイム・クリティカルでないコード・セクションの間に
順序づけを実行できる場合は、同期を削除できる。
例えば、1 つの変数を使用して、複数のスレッド上で発生するイベントをカウントする場合
を考える。以下のコードは、このようなプログラムを OpenMP で記述する 1 つの方法を示し
ている。
98
メモリ管理
5
int count=0;
#pragma omp parallel shared(count)
{
if (event_happened)
{
#pragma omp atomic
count++;
}
}
このプログラムは、イベントが発生するたびにコストがかかる。これは、count をインク
リメントするスレッドが一度に 1 つだけなのを保証するために、同期をとる必要があるため
である。イベントが発生するたびに、同期が行われる。同期を削除すれば、プログラムの実
行が高速化する。安全に同期を削除する 1 つの方法は、並列リージョン内で各スレッドに自
分のイベントをカウントさせ、後で個々のカウントを合計することである。以下のコード
は、この手法を示している。
int count=0;
int tcount=0;
#pragma omp threadprivate(tcount)
#pragma omp parallel
{
if (event_happened)
{
tcount++;
}
}
#pragma omp parallel shared(count)
{
#pragma omp atomic
count += tcount;
}
このプログラムは、各スレッド専用の tcount 変数を使用して、各スレッドのカウントを格
納する。最初の並列リージョンですべてのローカルイベントをカウントした後、次のリー
ジョンでこのカウントを全体のカウントに加算する。この解決策は、イベントごとの同期を
スレッドごとの同期で置き換える。従って、イベントの回数がスレッドの数よりはるかに大
きい場合は、パフォーマンスは向上する。
プログラムの遅れが許されない部分でスレッドごとのローカル・ストレージを使用する方法
には、もう 1 つの利点がある。それは、複数のプロセッサが 1 つのデータ・キャッシュを共
有していない場合、プロセッサのキャッシュ内でローカルデータは共有データより長時間有
効でいることである。複数のプロセッサのデータ・キャッシュ内に同じアドレスが存在する
場合、プロセッサのうち1つがそのアドレスに書き込むと、他のすべてのプロセッサのキャッ
シュ内で同じアドレスが無効化される。従って、他のプロセッサがそのアドレスにアクセス
するときは、アドレスはメモリから再び取り出される。しかし、スレッドごとのローカル
データは、他のプロセッサが書き込まないため、プロセッサのキャッシュ内にとどまりやす
い。
99
マルチスレッド・アプリケーションの開発
前のコード例は、OpenMP でスレッドごとのローカル・ストレージを指定する 1 つの方法を
示している。Pthreads で同じ処理を実行するには、プログラマは、以下の例のように、ス
レッドごとのローカル・ストレージにアクセスするためのキーを作成する必要がある。
#include <pthread.h>
pthread_key_t tsd_key;
<arbitrary data type> value;
if (pthread_key_create (&tsd_key, NULL))
err_abort(status, “Error creating key”);
if (pthread_setspecific( tsd_key, value))
err_abort(status, “Error in pthread_setspecific”);
value = (<arbitrary data type>)pthread_getspecific( tsd_key );
Win32 API では、プログラマは、以下の例のように、TlsAlloc を使用して TLS インデック
スを割り当て、次にそのインデックスを使用してスレッドごとのローカル値を設定する。
DWORD tls_index;
LPVOID value;
tls_index = TlsAlloc();
if (tls_index == TLS_OUT_OF_INDEXES)
err_abort( tls_index, “Error in TlsAlloc”);
status = TlsSetValue( tls_index, value );
if (status == 0)
err_abort( status, “Error in TlsSetValue”);
value = TlsGetValue (tls_index);
OpenMP では、parallel プラグマまたは threadprivate プラグマ上の private 節中で
スレッドごとのローカル変数を指定すれば、スレッドごとのローカル変数も作成できる。こ
れらの変数は、並列リージョンの終わりで自動的に解放される。もちろん、スレッドごとの
ローカルデータを指定するもう 1 つの方法は、スレッド化モデルに関係なく、所定のスコー
プ内のスタック上に割り当てられた変数を使用することである。これらの変数は、スコープ
の終わりで解放される。
推奨事項
スレッドごとのローカル・ストレージ手法を利用できる条件は、遅れが許されないコード・
セクション内で同期がコーディングされていることと、同期される操作をリアルタイムで順
序づけする必要がないことである。ただし、操作のリアルタイムの順序づけが重要であって
も、遅れが許されないセクションの間に十分な情報を収集でき、後でタイム・クリティカル
でないコード・セクションの間に順序づけを再現できる場合は、この手法を利用できる。
以下の例を考える。この例では、複数のスレッドが共有バッファにデータを書き込む。
100
メモリ管理
5
int buffer[ENTRIES];
main()
{
#pragma omp parallel
{
update_log (time, value1, value2);
}
}
void update_log (time, value1, value2)
{
#pragma omp critical
{
if (current_ptr + 3 > ENTRIES)
{
print_buffer_overflow_message ();
}
buffer[current_ptr] = time;
buffer[current_ptr+1] = value1;
buffer[current_ptr+2] = value2;
current_ptr += 3;
}
}
time は単調に増加する値とする。このバッファデータに関するプログラムの実際の要件は、
time に従ってソートしたバッファデータを時々ファイルに書き込むことだけである。この
場合、スレッドごとのローカルバッファを使用すれば、update_log ルーチン内の同期を
削除できる。各スレッドには、tpbuffer と tpcurrent_ptr の別々のコピーが割り当て
られる。これによって、update_log 内のクリティカル・セクションを削除できる。各種
のスレッドごとのプライベート・バッファから得られたエントリは、後でプログラムのタイ
ム・クリティカルでない部分で結合できる。
使用ガイドライン
開発者は、この手法に含まれるトレードオフに注意する必要がある。この手法は、同期を不
要にするわけではなく、遅れが許されないコード・セクションからタイム・クリティカルで
ないコード・セクションに同期を移動するだけである。第 1 に、同期が含まれる元のコー
ド・セクションの実行速度が、同期によって実際に大きく低下しているかどうか確認する
(インテル® VTune™ パフォーマンス・アナライザを使用して、パフォーマンス・プロファイ
ルを生成できる)
。第 2 に、操作の時間的順序づけがアプリケーションに重要かどうか確認
する。順序づけが重要でない場合は、イベントカウンティング・コードの例のように、同期
を削除できる。時間的順序づけが重要な場合は、順序づけを後で正しく再構成できるかどう
か検討する。第 3 に、コード内の他の場所に同期を移動した場合、新しい場所で同じような
性能上の問題が起こらないかを検証する。これを確認する方法の 1 つは、
(上記のイベント
カウンティングの例のように)コードの変更によって同期の数が著しい減少を示すことであ
る。
101
マルチスレッド・アプリケーションの開発
参考資料
本シリーズの参照個所 :
インテル® ソフトウェア開発製品、2.4「インテル® スレッド・チェッカーによ
るマルチスレッド・エラーの検出」
インテル® ソフトウェア開発製品、2.5「スレッド・プロファイラによる
OpenMP パフォーマンスの評価」
アプリケーションのスレッド化、3.5「見かけの依存関係の回避または解消に
よる並列性の露出」
他の参考資料 :
David R. Butenhof 著『Programming with POSIX Threads』、Addison-Wesley 刊、
1997 年
Johnson M. Hart 著『Win32 System Programming(2nd Edition)』、Addison-Wesley
刊、2001 年
Jim Beveridge、Robert Weiner 共著『Multithreading Applications in Win32』
、
Addison-Wesley 刊、1997 年
102
メモリ管理
5
5.3 スレッドスタックのオフセットによる、
ハイパー・スレッディング・テクノロジ対応インテル®
プロセッサ上のキャッシュ競合の回避
カテゴリ
メモリ管理
記述範囲
ハイパー・スレッディング・テクノロジ対応インテル ® プロセッサ上での Pthreads または
Win32* API によるマルチスレッド処理
キーワード
ハイパー・スレッディング・テクノロジ、キャッシュ・コヒーレンシ、データ・アライメン
ト、VTune™ アナライザ、スタックの割り当て
摘要
ハイパー・スレッディング・テクノロジ対応プロセッサは、複数の論理プロセッサ間で、
キャッシュ・ライン単位で 1 次データ・キャッシュを共有する。キャッシュ・ライン上のモ
ジュロ 64KB 間隔の仮想アドレスに頻繁にアクセスすると、別名競合が発生し、パフォーマ
ンスに悪影響を与える場合がある。スレッドスタックは、一般的にモジュロ 64KB の境界上
に作成されるため、スタックへのアクセスは頻繁に競合する。スタックの始点を調整する
と、これらの競合が軽減され、パフォーマンスが大きく向上する。ただし、64KB の別名競
合は、プロセッサのモデルに依存する。将来のプロセッサでは、モジュロ境界が調整される
か、この競合が完全に解消される可能性がある。
背景情報
ハイパー・スレッディング・テクノロジ対応インテル® プロセッサは、複数の論理プロセッ
サ間で 1 次データ・キャッシュを共有する。
モジュロ 64KB 間隔の仮想アドレスを持つキャッ
シュ・ラインは、1 次データ・キャッシュの同一スロットについて競合する。この状態は、
1 次データ・キャッシュのパフォーマンスと分岐予測ユニットの両方に影響を与える。64KB
の別名競合以外に、プロセッサ・コア・ロジックがモジュロ 1MB 間隔のアドレスを持つス
ペ キ ュレ ーテ ィ ブ・デ ータ を 使用 する 場 合、分 岐 の予 測ミ ス が増 える 可 能性 があ る。
Microsoft* Windows* オペレーティング・システム上では、現在のところ、スレッドスタック
は、デフォルトでは 1MB 境界の倍数上で作成される。非常によく似たスタック・フレーム・
イメージと(スタック上のローカル変数への)アクセスパターンを持つ 2 つのスレッドは、
別名競合の原因になり、パフォーマンスが大きく低下しそうである。将来のハイパー・ス
レッディング・テクノロジ対応インテル・プロセッサは、別名競合の両方の原因に対処する
予定である。簡単な回避策として、各スレッドの初期スレッドスタック・アドレスを調整す
れば、ハイパー・スレッディング・テクノロジ対応インテル・プロセッサ上でのアプリケー
ションのパフォーマンスはかなり回復する。
103
マルチスレッド・アプリケーションの開発
推奨事項
各スレッドについてスタック・オフセットを作成し、ハイパー・スレッディング対応プロ
セッサ上のスレッド間の 1 次データ・キャッシュ・ラインの競合を避ける。
ハイパー・スレッディング対応プロセッサ上で、これらの別名競合のためにアプリケーショ
ンのパフォーマンスが低下しているかどうかを確認するには、2 つの方法がある。第 1 の
(より決定的な)方法は、アプリケーションの性能テスト用ワークロードに対して、推奨し
た回避策を試してみることである。ハイパー・スレッディング・テクノロジを有効にした場
合と無効にした場合について、結果のパフォーマンスを比較すると、相対的なパフォーマン
スの差を直接に測定できる。第 2 の方法は、インテル® VTune™ パフォーマンス・アナライ
ザを使用する方法である。ハイパー・スレッディング・テクノロジを有効にした場合と無効
にした場合について、アプリケーションの性能テスト用ワークロードを実行し、クロック・
チック・イベントと 64KB の別名競合イベントの両方を収集する。
アプリケーション内のモジュールと関数を(クロックチックの高い方から順に)ソートした
後、64KB の別名イベントの数を比較する。ハイパー・スレッディング・テクノロジを有効
にした状態で、64KB の別名イベントの数が約 3 倍に増加するのは珍しいことではない。し
かし、1 つのモジュールまたは関数レベルで 8 倍以上の差が見られるアプリケーションは、
以下に説明する最適化手法によって、パフォーマンスが大きく向上することが実証されてい
る。問題のモジュールまたは関数の所要時間が、全実行時間のうち大きな割合を占める場合
は、これによってアプリケーション全体レベルのパフォーマンスが直ちに向上する。
ただし、インテル・プロセッサのハイパー・スレッディングのサポートを有効または無効に
するには、システム BIOS のサポートが必要である。BIOS ベンダによっては、ハイパー・
スレッディング機能を有効または無効にするユーザレベルの機能をサポートしていないこ
とがある。
通常、スレッドの作成は、オペレーティング・システム固有のアプリケーション・インター
フェイスを使用して、関数へのポインタとスレッドに固有のデータブロックへのポインタを
そのアプリケーション・インターフェイスに渡すと行われる。初期スレッドスタック・アド
レスの調整の鍵は、元の関数ポインタを、作成されたスレッド数によって決まる可変量だけ
スタックを調整する中間関数で置き換えることである。元のスレッド関数へのポインタ、ス
レッド id、元のパラメータ・データ・ブロックへのポインタを含む、新しい中間パラメー
タ・ブロックが必要とされる。この中間関数は、スタックアドレスを調整した後、元の関数
を呼び出して、元のスレッド固有のパラメータ・データを受け渡す。関数ポインタを含む新
しいパラメータ・ブロックを使用したコードは、汎用性が高いため、異なるスレッド関数を
起動するスレッドプールに対して使用できる。これより汎用性の低い代替手段として、関数
ポインタの使用を避けて、中間関数に元の関数を直接呼び出させることもできる。ただし、
コンパイラは新しいスレッド関数の中で元のスレッド関数をインライン展開しないのに注
意する必要がある。元のスレッド関数が「インライン展開」されている場合は、元の関数に
対する調整したスタックアドレスのメリットは失われる。中間関数と関数ポインタを合わせ
て使用すれば、コンパイラはコンパイル時にどの関数をインライン展開するかを決定できな
いため、この問題は避けられる。
104
メモリ管理
5
各スレッドの初期スタックアドレスを調整する最も簡単な方法は、中間スレッド関数内でさ
まざまなバイト数を指定して、メモリ割り当て関数 _alloca を呼び出すことである。
_alloca 関数は、メモリをスタック上に直接割り当てる。_alloca 関数に渡されるバイト
数を 調整すれば、次の関数の開始スタ ックアドレスを調整で きる。_alloca 関数は、
malloc.h ヘッダファイル内で指定される。この手法を使用してスタックアドレスを調整す
ると、各スレッドのスタックフレーム内に、使用されない仮想メモリが割り当てられる。
コード例 9 では、1KB のオフセットにスレッド ID 番号を掛けた量だけ、スレッド・スタッ
クフレームをオフセットしている。1KB は魔法の数字ではないが、一般的に各種のアプリ
ケーションで有効である。重要な注意点の 1 つは、Microsoft Windows オペレーティング・シ
ステムの現在のバージョンでは、特定のプロセスがアクセスできる仮想メモリ容量が制限さ
れていることである。仮想メモリの制限がアプリケーションに重要な影響を与える場合は、
最適なオフセットを計算するか、この制約条件の範囲内でこの手法を修正する必要がある。
105
マルチスレッド・アプリケーションの開発
コード例 9. _alloca を使用したスレッドスタックのオフセットによってキャッシュ競合を
回避するコード
// Original thread parameter data structure
struct ParameterBlk
{
int thread_specific_data;
// Padding to keep thread data at least a cache-line apart
char padding[2 * CACHE_LINE_SZ ? sizeof (int)];
};
typedef DWORD (*PFI) (void*);
// Structure containing arguments provided to each thread
struct FunctionBlk
{
PFI ThreadFuncPtr;
struct ParameterBlk* function_parameters;
unsigned int thread_number;
// Padding to keep thread data at least a cache-line apart
char padding[2 * CACHE_LINE_SZ ? sizeof (PFI) sizeof(struct ParameterBlk*) sizeof(unsigned int)];
};
DWORD WINAPI OriginalThreadProc (LPVOID ptr)
{
// This would have been the original thread function
return 0;
}
#define STACK_OFFSET 1024
DWORD WINAPI IntermediateThreadProc (LPVOID ptr)
{
struct FunctionBlk* parameter = (struct FunctionBlk*) ptr;
// Adjusting stack address
_alloca (parameter->thread_number * STACK_OFFSET);
// Calling original thread procedure using a function pointer.
// You could call the function directly as shown blow but be
// careful that the function doesnÅft get inlined.
return
(*parameter->ThreadFuncPtr)(parameter->
function_parameters);
}
作成するスレッドの数を決定する際は、メインスレッドを使用してその仕事の一部を行わせ
ることを推奨する。メインスレッドが既に持っているスタックフレーム・イメージとデータ
アクセス・パターンは、通常は子スレッドとは非常に異なる(子スレッドは、1MB 境界に
アライメントされたクリーンなスタックフレームから始まる)。さらに、同期と管理の対象
となる子スレッドの数が 1 つ少なくなる。
ただし、メインスレッドが他のタスクの管理やユーザ入力の処理を担当する場合は、この方
法は望ましくない。
106
5
メモリ管理
使用ガイドライン
スレッドスタック・オフセットのシングル・ソースコードは、パフォーマンスへの影響なし
にマルチプロセッサ・システムに使用できる。しかし、スタック・オフセットを使用する
と、アプリケーションが利用できる仮想メモリが全体として減少する。一般的に、この問題
は、多数のスレッドを実行する非常に大規模なアプリケーションにのみ影響を与える。ス
タック・オフセットの量を調整すると、パフォーマンスと仮想メモリ容量のバランスをとれ
る。
スタック・オフセットの最適なサイズは、アプリケーションによって異なる。
(ローカル変
数とこれ以降の関数呼び出しのために)深いスレッドスタックを持つスレッド関数や、ルー
プ内で大きなローカルデータ構造を操作するスレッド関数は、スタック・オフセットのサイ
ズが大きい方がパフォーマンスが向上する傾向がある。逆に、スタックサイズが小さいス
レッド関数は、小さいスタック・オフセットで高性能を発揮する。一般的に、1 スレッド当
たり 1KB ずつスタック・オフセットを大きくすれば、多くのアプリケーションで満足のい
く結果が得られる。
参考資料
本シリーズの参照個所 :
インテル® ソフトウェア開発製品、2.3「VTune™ パフォーマンス・アナライザ
によるスレッド間のフォルス・シェアリングの回避と特定」
本章、5.1「スレッド間のヒープの競合の回避」
他の参考資料 :
Phil Kerly 著『Adjusting Thread Stack Address To Improve Performance on Intel®
Xeon™Processors』(http://developer.intel.com)
107
マルチスレッド・アプリケーションの開発
108
〒300-2635 茨城県つくば市東光台 5-6
http://www.intel.co.jp/
©2003–2004, Intel Corporation. 無断での引用、転載を禁じます。
2004 年 1 月
420J-001