MSVC 2010 デバッグ モードで OpenMP を使用してビルドすると、奇妙な動作が発生するかなり複雑なプログラムがあります。実際のプログラムの構造を模倣した次の最小限の作業例 (実際には最小限ではありません) を作成するために最善を尽くしました。
#include <vector>
#include <cassert>
// A class take points to the whole collection and a position Only allow access
// to the elements at that posiiton. It provide read-only access to query some
// information about the whole collection
class Element
{
public :
Element (int i, std::vector<double> *src) : i_(i), src_(src) {}
int i () const {return i_;}
int size () const {return src_->size();}
double src () const {return (*src_)[i_];}
double &src () {return (*src_)[i_];}
private :
const int i_;
std::vector<double> *const src_;
};
// A Base class for dispatch
template <typename Derived>
class Base
{
protected :
void eval (int dim, Element elem, double *res)
{
// Dispatch the call from Evaluation<Derived>
eval_dispatch(dim, elem, res, &Derived::eval); // Point (2)
}
private :
// Resolve to Derived non-static member eval(...)
template <typename D>
void eval_dispatch(int dim, Element elem, double *res,
void (D::*) (int, Element, double *))
{
#ifndef NDEBUG // Assert that this is a Derived object
assert((dynamic_cast<Derived *>(this)));
#endif
static_cast<Derived *>(this)->eval(dim, elem, res);
}
// Resolve to Derived static member eval(...)
void eval_dispatch(int dim, Element elem, double *res,
void (*) (int, Element, double *))
{
Derived::eval(dim, elem, res); // Point (3)
}
// Resolve to Base member eval(...), Derived has no this member but derived
// from Base
void eval_dispatch(int dim, Element elem, double *res,
void (Base::*) (int, Element, double *))
{
// Default behavior: do nothing
}
};
// A middle-man who provides the interface operator(), call Base::eval, and
// Base dispatch it to possible default behavior or Derived::eval
template <typename Derived>
class Evaluator : public Base<Derived>
{
public :
void operator() (int N , int dim, double *res)
{
std::vector<double> src(N);
for (int i = 0; i < N; ++i)
src[i] = i;
#pragma omp parallel for default(none) shared(N, dim, src, res)
for (int i = 0; i < N; ++i) {
assert(i < N);
double *r = res + i * dim;
Element elem(i, &src);
assert(elem.i() == i); // Point (1)
this->eval(dim, elem, r);
}
}
};
// Client code, who implements eval
class Implementation : public Evaluator<Implementation>
{
public :
static void eval (int dim, Element elem, double *r)
{
assert(elem.i() < elem.size()); // This is where the program fails Point (4)
for (int d = 0; d != dim; ++d)
r[d] = elem.src();
}
};
int main ()
{
const int N = 500000;
const int Dim = 2;
double *res = new double[N * Dim];
Implementation impl;
impl(N, Dim, res);
delete [] res;
return 0;
}
vector
実際のプログラムにはetcはありませんが、 Element
、Base
、Evaluator
およびImplementation
は実際のプログラムの基本的な構造を捉えています。デバッグ モードでビルドし、デバッガーを実行すると、アサーションが で失敗しPoint (4)
ます。
コール スタックを表示して、デバッグ情報の詳細を次に示します。
に入るPoint (1)
と、ローカルi
には値がありますが371152
、これで問題ありません。変数elem
はフレームに表示されません。これは少し奇妙です。でも at のアサーションPoint (1)
は失敗しないので大丈夫だと思います。
その後、クレイジーなことが起こりました。eval
byへの呼び出しはEvaluator
その基本クラスに解決されるため、実行Point (2)
されました。この時点で、デバッガーは has を示していelem
ます。これは、値によってに渡す前に作成するために使用されなくi_ = 499999
なりました。次のポイントは に解決されますが、今回はがあり、これは範囲外です。これは、コールが に向けられ、アサーションに失敗したときの値です。i
elem
Evaluator
Base::eval
Point (3)
elem
i_ = 501682
Point (4)
Element
オブジェクトが値渡しされるたびに、そのメンバーの値が変更されるように見えます。プログラムを複数回再実行すると、常に再現できるとは限りませんが、同様の動作が発生します。実際のプログラムでは、このクラスはイテレータのように設計されており、パーティクルのコレクションを反復処理します。それが反復するものはコンテナのように正確ではありませんが。とにかく、ポイントは、値によって効率的に渡されるのに十分小さいということです。したがって、クライアント コードは、何らかの参照またはポインターの代わりに の独自のコピーがあることを認識しており、書き込みアクセスのみを提供する のインターフェイスElement
に固執する限り、スレッド セーフについて (あまり) 心配する必要はありません。Element
コレクション全体の単一の位置に。
GCC と Intel ICPC で同じプログラムを試しました。予期しないことは何も起こりません。実際のプログラムでは、正しい結果が生成されます。
どこかで OpenMP を間違って使用しましたか? elem
作成された at aboutPoint (1)
はループ本体に対してローカルになると思いました。また、プログラム全体では、N
生成された以上の価値はありませんが、その新しい価値はどこから来るのでしょうか?
編集
デバッガーをより注意深く調べたところ、値で渡されelem.i_
たときに変更されたのにelem
、ポインターelem.src_
が変更されていないことがわかりました。値渡し後、(メモリアドレスの) 同じ値を持つ
編集:コンパイラフラグ
CMake を使用して MSVC ソリューションを生成しました。正直に言うと、MSVC や Windows の一般的な使い方がわかりません。私がそれを使用している唯一の理由は、多くの人がそれを使用していることを知っているため、問題を回避するためにライブラリをテストしたいからです.
CMake で生成されたプロジェクトは、Visual Studio 10 Win64
ターゲットを使用して、コンパイラ フラグが次のように表示されます
/DWIN32 /D_WINDOWS /W3 /Zm1000 /EHsc /GR /D_DEBUG /MDd /Zi /Ob0 /Od /RTC1
。プロパティ ページ - C/C++ - コマンド ラインにあるコマンド ラインは次のとおりです。
/Zi /nologo /W3 /WX- /Od /Ob0 /D "WIN32" /D "_WINDOWS" /D "_DEBUG" /D "CMAKE_INTDIR=\"Debug\"" /D "_MBCS" /Gm- /EHsc /RTC1 /MDd /GS /fp:precise /Zc:wchar_t /Zc:forScope /GR /openmp /Fp"TestOMP.dir\Debug\TestOMP.pch" /Fa"Debug" /Fo"TestOMP.dir\Debug\" /Fd"C:/Users/Yan Zhou/Dropbox/Build/TestOMP/build/Debug/TestOMP.pdb" /Gd /TP /errorReport:queue
ここで疑わしいものはありますか?