OpenMPでshared指示できるもの、できないもの

はじめに

C++でOpenMPを使った並列化をする際、parallelリージョンでクラスのメンバ変数をshared指定できないのだが、そのことを書いた日本語の記事が、大昔に自分で書いた記事しか見つけられなかったので、あらためてこっちにも書いておく。

現象

クラスのメンバ変数をOpenMPのshared指定するとエラーで怒られる。例えばこんなコード。

#include <iostream>
#include <omp.h>
#include <vector>
class Hoge {
private:
  std::vector<int> v;
public:
  void func(void) {
    int tid;
    #pragma omp parallel shared(v) private(tid)
    {
      tid = omp_get_thread_num();
      #pragma omp critical
      v.push_back(tid);
    }
    for (unsigned int i = 0; i < v.size(); i++) {
      std::cout << v[i] << std::endl;
    }
  }
};
int
main(void) {
  Hoge h;
  h.func();
}

これは、スレッド番号を自分のメンバ変数であるstd::vector<int>に貯めることを意図したコードだが、g++でコンパイルするとこんなエラーが出る。

$ g++ -fopenmp test.cpp 
test.cpp: メンバ関数 ‘void Hoge::func()’ 内:
test.cpp:10:48: エラー: ‘Hoge::v’ is not a variable in clause ‘shared’
     #pragma omp parallel shared(v) private(tid)

icpcだとこんなエラーになる。

$ icpc -fopenmp test.cpp 
test.cpp(10): error: invalid entity for this variable list in omp clause
      #pragma omp parallel shared(v) private(tid)
                                  ^

compilation aborted for test.cpp (code 2)

回避策1

一度テンポラリな変数に受けてからコピーすれば大丈夫。例えばこんな感じ。

#include <iostream>
#include <omp.h>
#include <vector>
class Hoge {
private:
  std::vector<int> v;
public:
  void func(void) {
    int tid;
    std::vector<int> vtmp;
    #pragma omp parallel shared(vtmp) private(tid)
    {
      tid = omp_get_thread_num();
      #pragma omp critical
      vtmp.push_back(tid);
    }
    for (unsigned int i = 0; i < vtmp.size(); i++) {
      v.push_back(vtmp[i]);
    }
    for (unsigned int i = 0; i < v.size(); i++) {
      std::cout << v[i] << std::endl;
    }
  }
};
int
main(void) {
  Hoge h;
  h.func();
}
$ g++ -fopenmp test2.cpp  

$ OMP_NUM_THREADS=4 ./a.out 
1
2
0
3

回避策2

どうせ無指定の変数はsharedになるはずであるし、critical指定していれば良いはずなので、shared指定を外すという方法もある。

#include <iostream>
#include <omp.h>
#include <vector>
class Hoge {
private:
  std::vector<int> v;
public:
  void func(void) {
    int tid;
    #pragma omp parallel private(tid)
    {
      tid = omp_get_thread_num();
      #pragma omp critical
      v.push_back(tid);
    }
    for (unsigned int i = 0; i < v.size(); i++) {
      std::cout << v[i] << std::endl;
    }
  }
};
int
main(void) {
  Hoge h;
  h.func();
}

なんか不安なのだが、とりあえず正しく動いているように見える。

原因?

クラスのメンバ変数はOpenMPでshared指定できず、一度テンポラリ変数に落として後でコピーする必要がある。これについて、そのものズバリの回答(というか仕様)を見つけられないのだが、StackOverflowでの回答その1その2によれば、

  • クラス変数は、実行時までインスタンス化されていない
  • なのでコンパイラは、コンパイル時にそのアドレスを知ることができない
  • sharedなら良いが、private化するにはデータをコピーしなければならず、それはできないからエラーになる

という理屈らしい。ただ、これだとprivate指定してエラーになるのはわかるが、上記のようにshared指定したらエラーになることの説明ができない気がする?

staticメンバとconst変数

コンパイル時にアドレスが決まっていればいいの?ということで、メンバ変数をstaticにしてみる。

#include <iostream>
#include <omp.h>
#include <vector>
class Hoge {
private:
  static std::vector<int> v;
public:
  void func(void) {
    int tid;
    #pragma omp parallel shared(v) private(tid)
    {
      tid = omp_get_thread_num();
      #pragma omp critical
      v.push_back(tid);
    }
    for (unsigned int i = 0; i < v.size(); i++) {
      std::cout << v[i] << std::endl;
    }
  }
};
std::vector<int> Hoge::v;
int
main(void) {
  Hoge h;
  h.func();
}

これをg++に食わすとエラーになる。

$ g++ -fopenmp test4.cpp
test4.cpp: In member function 'void Hoge::func()':
test4.cpp:10:48: error: 'Hoge::v' is predetermined 'shared' for 'shared'

これはこういうことが起きているらしい。

  • コンパイラはHoge::vがstaticなので、デフォルトでsharedだと判断する
  • なのにプログラマが明示的にshared指定したので「二重指定だ」と怒る

コンパイラの判断と一致しているのだから怒ることないじゃん、と思うのだが・・・

なお、staticメンバでなく、const変数についてもg++はデフォルトでshared判定をするため、プログラマが明示的にshared指定すると「二重指定だ!」と怒られる。

#include <iostream>
#include <omp.h>

const int c = 1;

int
main(void){
  int x[10];
#pragma omp parallel for shared(c)
  for(int i=0;i<10;i++){
    x[i] = c;
  }
  for(int i=0;i<10;i++){
    std::cout << x[i] << std::endl;
  }
}
$ g++ -fopenmp test5.cpp
test5.cpp: In function 'int main()':
test5.cpp:9:35: error: 'c' is predetermined 'shared' for 'shared'

ちなみに、インテルコンパイラ(icpc)だと、staticメンバのshared指定についてはエラー、const変数のshared指定については通る。

$ icpc -fopenmp test4.cpp # 怒る

test4.cpp(10): error: invalid entity for this variable list in omp clause
      #pragma omp parallel shared(v) private(tid)
                                  ^

compilation aborted for test4.cpp (code 2)

$ icpc -fopenmp test5.cpp # 通る

さらに言えば、富士通コンパイラだと、メンバ変数のshared指定はできないが、staticメンバもconst変数もshared指定できる。

まとめるとこんな感じ。

shared指定するもの g++ インテルコンパイラ 富士通コンパイラ
メンバ変数 × × ×
staticメンバ × ×
const変数 ×

まとめ

OpenMPよくわかんない。