OpenMP* を使用中に変数アクセスでスレッドがクラッシュしないようにする

同カテゴリーの次の記事

インテル® MKL を Numpy/Scipy に実装

この記事は、インテル® デベロッパー・ゾーンに掲載されている「Stop Threads from Clashing Over Variables in OpenMP」の日本語参考訳です。


OpenMP* を使用して、複数のスレッドにコードブロックの複製 (ループや単純なブロック) を割り当てることができます。その際、データの扱いを容易にするため、各スレッドは変数をローカルに持つことができます。この記事では、その方法を説明します。

前回、OpenMP* について簡単に説明し、1 つの文 (または 1 つのブロック文) を利用可能なすべてのコアに複製するようにしました。ここでは、各スレッドがローカルコピーを持つことができるように変数を宣言する方法を説明します。また、マルチスレッドや並列プログラミングで上手く動作しない stdout を使用せずに、OpenMP ランタイム・ライブラリーを利用します。

OpenMP* ランタイム・ライブラリー

OpenMP* によるプログラミングでは、C++ 言語拡張ではなく、プラグマを利用します。つまり、コンパイラーはプラグマを認識できなければなりません。さらに、OpenMP* のメカニズムを構成するコードは、ライブラリーとしてプログラムにリンクされます。このコードには、並列処理を可能にするコードだけでなく、OpenMP* が提供する API 関数が含まれています。

関数を使用するには、次のようにヘッダーファイルをインクルードする必要があります。

#include <omp.h>

これにより、コンパイラーに利用可能な関数を知らせ、それらの関数を呼び出すことができます。その 1 つに、実行中のプログラムに利用可能なプロセッサー数を返す関数があります。以下は、プロセッサー数を出力する簡単なプログラムです。

#include <iostream>
#include <omp.h>
using namespace std;
int main() {
    cout << omp_get_num_procs() << endl;
    return 0;
}

OpenMP* API 関数は、omp_ で始まります。ここで呼び出している omp_get_num_procs 関数は、プロセッサー数を返します。インテル® Core™ アーキテクチャー・ベースのクアッドコア・ノートブックでこのコードを実行すると、次の結果が返されます。

8

Go Parallel を愛読されている方は、ここで 4 ではなく 8 が返される理由がお分かりでしょう。オペレーティング・システムによって返されるプロセッサー数には、ハイパースレッディングにより有効となる論理プロセッサー数が含まれます。テストに使用したノートブックには 4 つのコアがあり、各コアは 2 つのハイパースレッドをサポートしているため、合計スレッド数は 8 になります。オペレーティング・システムは 8 を返しますが、ハイパースレッドの存在を認識し、各コアに 2 つのスレッドがあることを把握しています。

これは、作業を一部のコアにまとめてオーバーロードさせる代わりに、複数のコアに分配するためです。例えば、負荷の大きなマルチスレッド化された数値計算を行うアプリケーションにおいてプロセッサーを最大限に利用する方法を考えてみます。クアッドコア・マシンで 4 スレッドをスポーンする場合、2 つのコアの 4 スレッドを使用するよりも、4 つの物理コアのスレッドを使用したほうが 良いパフォーマンスが得られます (このとき、一般に、どのスレッドを使用するかは指示しません。オペレーティング・システムによって決定されます)。

次に、プライベート変数と呼ばれる変数について見てみましょう。プライベート変数は、コードブロックの範囲外 (並列領域外) で宣言され、各スレッドが変数のプライベート・コピーを保持します (これは、レデューサーを使用するための 1 つのステップです)。

コードブロックで変数を宣言し、そのブロック内でスレッドをスポーンするときに、各スレッドが変数のローカルコピーを保持するようにプラグマに節を追加できます。以下のコードは、usedthreads と id という 2 つの変数を宣言し、プラグマによりそれらを並列に使用できるようにする方法を示します。

#include <iostream>
#include <omp.h>
using namespace std;
int main(void)
{
  const int MAXTH = omp_get_num_procs();
  bool usedthreads[MAXTH];
  for (int i=0; i<MAXTH; i++) {
    usedthreads[i] = false;
  }
  int id;
  #pragma omp parallel private(id) shared(usedthreads)
  {
    id = omp_get_thread_num();
    usedthreads[id] = true;
  }
  for (int i=0; i<MAXTH; i++) {
    cout << usedthreads[i] << endl;
  }
  return 0;
}

このコードは、スレッド ID を取得して対応する配列の値を true に設定します。スレッド ID を取得するため、OpenMP* API 関数 omp_get_thread_num を呼び出し、その戻り値を変数 id に格納しています。変数 id は、データ競合を防ぐため、parallel 構文に private(id) 節を追加して各スレッドのローカル変数として割り当てを指示しています。一方、usedthreads 配列は、shared(usedthreads) 節を追加してすべてのスレッドが共有できるように宣言しています。

まとめ

OpenMP* には、コードを並列化するためのさまざまなプラグマと節があります。また、omp.h ヘッダーファイルには API 関数のセットも含まれています。開発システム上で omp.h を検索し、ファイルを開くと、利用可能な関数を確認できます (Visual Studio* では、#include 行で omp.h を右クリックするだけで簡単に確認できます)。ただし、OpenMP* 対応コンパイラーでもサポートに違いがあるため、どのコンパイラーが提供するヘッダーファイルであるか確認してください。

ここでは、概要のみを説明しました。次回は、ループの並列化について説明します。その後は、OpenMP* に関して皆さんの興味のあるトピックやより高度なトピックについて取り上げたいと思います。

関連記事 (英語)

「OpenMP: Parallel Programming Alternative」

「Taking OpenMP Out for a Spin」

関連記事