2-2 スレッド化 API の同期化ルーチンの使用

概要

開発者によっては、同期に伴うオーバーヘッドを削減したり、既存の構造にはない機能を追加するために、スレッド API の同期ルーチンの代わりに、独自コードの同期ルーチンを使用することもあるでしょう。このようなコードの同期ルーチンは、マルチスレッド・アプリケーションのパフォーマンスやパフォーマンス・チューニング、デバッグに悪影響を与える場合があります。

この記事は、「マルチスレッド・アプリケーションの開発のためのインテル・ガイド」の一部で、インテル® プラットフォーム向けにマルチスレッド・アプリケーションを効率的に開発するための手法について説明します。

はじめに

スレッド API の同期ルーチンに伴うオーバーヘッドを回避したり、スレッド API の同期ルーチンで必要な機能が提供されていない場合に、独自コードによる同期が行われことはよくあります。しかしながら、それらのコードによる同期には、スレッド API ルーチンを使用する場合と比べて、いくつの重大なデメリットかあります。

その 1 つとして、独自コードの同期では、異なるハードウェア・アーキテクチャーやオペレーティング・システムで優れたパフォーマンスが得られにくいことが挙げられます。この問題を例示したスピンロックの C コードについて考えてみましょう。

#include 
void acquire_lock (int *lock)
{
  while (_InterlockedCompareExchange (lock, TRUE, FALSE) == TRUE);
}
void release_lock (int *lock)
{
  *lock = FALSE;
}

_InterlockedCompareExchange コンパイラー組み込み関数は、実行中にほかのスレッドがメモリー・ロケーションへの変更を行わないようにするメモリー・インターロック命令です。第 1 引数で指定されたメモリーアドレスにある値と第 3 引数の値を比較して一致する場合は、第 2 引数の値を第 1 引数で指定されたメモリーアドレスにストアします。この関数は、指定されたメモリーアドレスのオリジナルの値を返します。acquire_lock ルーチンは、メモリー・ロケーション lock の値がアンロック状態 (FALSE) になるまでスピンし、アンロック状態になったらロックを取得 (lock の値を TRUE に設定) します。release_lock ルーチンは、メモリー・ロケーション lock の値を FALSE に戻して、ロックを解放します。

このコードは単純で効率的に見えますが、いくつかの問題があります。

  • 複数のスレッドが同じメモリー・ロケーションでスピンすると、ロック解放時にキャッシュ無効化とメモリー・トラフィックが頻発し、スレッド数が増えるほどスケーラビリティーが低下します。
  • このコードで使用しているアトミックなメモリー更新手法は、特定のプロセッサー・アーキテクチャーでは利用できないため、移植性が制限されます。
  • インテル® ハイパースレッディング・テクノロジーなどの特定のプロセッサー・アーキテクチャー機能では、スピンループの多用はパフォーマンスの低下につながります。
  • オペレーティング・システムには、while ループが有益な計算を行っているように見えるため、オペレーティング・システムのスケジューリングの公平性に悪影響を与えます。

これらの問題にはすべて解決方法がありますが、通常それらを使用すると、コードが複雑になる傾向があり、正当性の検証が難しくなります。また、移植性を維持しながらコードのチューニングを行うことは困難です。そのため、これらの問題はスレッド API に任せたほうが得策です。スレッド API では、移植性とスケーラビリティーを実現するために、多大な時間をかけて同期構造の検証とチューニングが行われています。

独自コードによる同期のもう 1 つの問題として、スレッド化ツールの精度を低下させることが挙げられます。例えば、インテル® Parallel Studio は、同期構造を特定して、マルチスレッド・アプリケーションのパフォーマンス (インテル® Parallel Amplifier を使用) と正当性 (インテル® Parallel Inspector を使用) に関する情報を提供します。

通常、スレッド化ツールは、サポートしているスレッド API の同期構造の機能を特定し、判断するように設計されています。上記の例のように、標準的な同期 API を使用していない場合、スレッド化ツールが同期を特定し、理解することは困難です。

スレッド化ツールによっては、独自コードの同期を特定するために、プログラマーがツール固有の宣言子、プラグマ、API 呼び出しなどの形式でヒントを指定できることがあります。ただし、このようなヒントがサポートされている場合でも、同期 API を使用する場合と比べると、アプリケーション・プログラムの分析の精度は低下し、パフォーマンス問題の検出が困難であったり、正当性ツールでデータ競合や同期の欠落が誤って報告されることがあります。

アドバイス

独自コードによる同期はできるだけ避けて、代わりに、スレッド API のルーチンを使用してください。例えば、インテル® スレッディング・ビルディング・ブロック (インテル®TBB) の queuing_mutexspin_mutex、OpenMP* の omp_set_lock/omp_unset_lock 宣言子や critical/end critical 宣言子、Pthreads* の pthread_mutex_lock/pthread_mutex_unlock を使用します。スレッド API の同期ルーチンと同期構造を吟味した上で、アプリケーションに適したものを使用するようにしてください。

スレッド API に必要な機能を備えた同期構造がない場合は、同期を減らしたり、同期の方法を変更するなど、アルゴリズムを見直してみてください。経験豊富なプログラマーは、API の簡単な同期構造を基に、独自の同期構造を作成することもできます。パフォーマンス問題のために独自コードによる同期を行う必要がある場合は、前処理済みの宣言子を使用して、独自コードの同期を同等の機能のスレッド API に容易に置換できるようにすることで、スレッド化ツールの精度を向上できます。

利用ガイド

API を利用した簡単な同期構造から、独自の同期構造を作成する場合は、パフォーマンスのスケーラビリティーを損ねないように、共有ロケーションでスピンループは使用しないでください。コードを移植する必要がある場合は、アトミックなメモリー・プリミティブの使用も避けたほうが良いでしょう。スレッド化のパフォーマンス解析/正当性検証ツールは、API の簡単な同期構造を正しく特定できても、それを基に作成された独自の同期構文を正しく特定できないことがあります。この場合、ツールの精度は低下します。