2012-07-13 19 views
11

我有一个相当复杂的程序,在MSVC 2010调试模式下使用OpenMP进行生成时会出现奇怪的行为。我尽我所能来构建下面的最小工作示例(尽管它并不是最小的),它将真正的程序的结构缩小了。使用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等。但ElementBaseEvaluatorImplementation捕捉真正的程序的基本结构。当以调试模式构建并运行调试器时,断言在Point (4)处失败。

这里是调试信息的一些细节,通过查看调用栈,

在进入Point (1),当地i具有价值371152,这是罚款。变量elem没有出现在框架中,这有点奇怪。但由于Point (1)的断言并不失败,我想这很好。

然后,发生了疯狂的事情。 evalEvaluator的呼叫解析为其基类,因此Point (2)被执行。此时,debugers显示elem具有i_ = 499999,它不再是i,用于在Evaluator中创建elem,然后通过值,值为Base::eval。下一点,它解析为Point (3),这一次,elem具有i_ = 501682,这是超出范围的,这是当调用指向Point (4)并且断言失败时的值。

它看起来像只要Element对象通过值传递,它的成员的值被改变。重新运行该程序多次,类似的行为发生虽然不总是可重复的。在真正的程序中,这个类被设计成像迭代器一样,迭代器遍历一系列粒子。尽管它迭代的东西并不像容器那样脆弱。但无论如何,重要的是它足够小,可以有效地通过价值传递。因此,客户端代码知道它拥有自己的Element副本,而不是一些引用或指针,并且只要他坚持使用Element的接口,就不必担心线程安全(很多),它只提供将访问权限写入整个集合的单个位置。

我尝试了与GCC和Intel ICPC相同的程序。没有发生不可预料的事情。在真正的程序中,产生正确的结果。

我在某处错误地使用了OpenMP吗?我认为在Point (1)创建的elem应该是循环体的本地。另外,在整个计划中,没有产生大于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

有什么suspecious这里?

+0

当某些代码编译为Release并且某些编译为Debug时,有时会出现奇怪的事情。您正在使用的OpenMP是否与您的程序使用相同的标志/调试内容编译? – 2012-07-13 23:04:56

+0

我不确定这个问题。除测试外,我通常不使用msvc。但是上面的代码是一个单一的文件程序。所以我猜无论使用哪个标志,它都用于整个程序。调试模式openmp有特殊选项吗?我用cmake来查找openmp标志,结果是/ openmp。 @SethCarnegie – 2012-07-13 23:15:27

+0

您是使用该文件编译OpenMP还是使用另一次编译的库? – 2012-07-13 23:16:09

回答

8

显然,MSVC中的64位OpenMP实现与代码不兼容,编译时没有进行优化。

要调试您的问题,我已经修改了代码,以节省只是调用this->eval()前的迭代次数为threadprivate全局变量,然后添加在Implementation::eval()开始检查,看是否保存的迭代次数不同于elem.i_

static int _iter; 
#pragma omp threadprivate(_iter) 

... 
#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) 
     _iter = i;    // Save the iteration number 
     this->eval(dim, elem, r); 
    } 
} 
... 

... 
static void eval (int dim, Element elem, double *r) 
{ 
    // Check for difference 
    if (elem.i() != _iter) 
     printf("[%d] _iter=%x != %x\n", omp_get_thread_num(), _iter, elem.i()); 
    assert(elem.i() < elem.size()); // This is where the program fails Point (4) 
    for (int d = 0; d != dim; ++d) 
     r[d] = elem.src(); 
} 
... 

似乎的elem.i_该随机值变得在不同的线程传递给void eval_dispatch(int dim, Element elem, double *res, void (*) (int, Element, double *))的值的坏混合物。这种情况在每次运行中都会发生几次,但只有elem.i_的值变得足够大才能触发断言时才会看到它。有时会发生这样的情况:混合值不会超过容器的大小,然后代码在没有声明的情况下完成执行。你也可以在断言之后的调试会话中看到VS调试器无法正确处理多线程代码:)

这只发生在未优化的64位模式下。它不会发生在32位代码中(调试和发布)。除非优化禁用,否则64位版本代码中也不会发生这种情况。

#pragma omp parallel for default(none) shared(N, dim, src, res) 
    for (int i = 0; i < N; ++i) { 
     ... 
#pragma omp critical 
     this->eval(dim, elem, r); 
    } 
} 

,但这样做会取消的OpenMP的好处:如果还有人把调用this->eval()在关键节不会发生。这表明进一步向下调用链的方式是以不安全的方式执行的。我检查了汇编代码,但找不到确切的原因。我真的很困惑,因为MSVC使用简单的按位复制(它甚至是内联)实现了Element类的隐式拷贝构造函数,并且所有操作都在堆栈上完成。

这让我想起Sun公司(现在是甲骨文公司)的编译器坚持认为,如果启用OpenMP支持,应该提高优化级别。不幸的是,MSDN中/openmp选项的文档没有提及可能来自“错误”优化级别的干扰。这也可能是一个错误。如果我可以访问另一个版本的VS,我应该测试它。

编辑:我按照承诺深入挖掘并在英特尔Parallel Inspector 2011中运行代码。它按预期发现了一种数据竞争模式。显然,当执行这一行:

this->eval(dim, elem, r); 

的是Windows 64位ABI需要创建并通过了地址elem临时副本到eval()方法。这里出现了一个奇怪的事情:这个临时副本的位置不在实现并行区域的funclet堆栈上(MSVC编译器按照它的方式调用它),而是将其地址作为第一个参数的funclet。由于这一论点是一个和所有线程是相同的,这意味着该临时副本被进一步传递到this->eval()实际上是所有线程之间共享,这是荒谬的,但仍然是事实作为一个可以很容易地观察到:

... 
void eval (int dim, Element elem, double *res) 
{ 
    printf("[%d] In Base::eval() &elem = %p\n", omp_get_thread_num(), &elem); 
    // Dispatch the call from Evaluation<Derived> 
    eval_dispatch(dim, elem, res, &Derived::eval); // Point (2) 
} 
... 

... 
#pragma omp parallel for default(none) shared(N, dim, src, res) 
    for (int i = 0; i < N; ++i) { 
     ... 
     Element elem(i, &src); 
     ... 
     printf("[%d] In parallel region &elem = %p\n", omp_get_thread_num(), &elem); 
     this->eval(dim, elem, r); 
    } 
} 
... 

运行该代码产生类似这样的输出:

[0] Parallel region &elem = 000000000030F348 (a) 
[0] Base::eval() &elem = 000000000030F630 
[0] Parallel region &elem = 000000000030F348 (a) 
[0] Base::eval() &elem = 000000000030F630 
[1] Parallel region &elem = 000000000292F9B8 (b) 
[1] Base::eval() &elem = 000000000030F630 <---- !! 
[1] Parallel region &elem = 000000000292F9B8 (b) 
[1] Base::eval() &elem = 000000000030F630 <---- !! 

正如预期elem具有每个线程执行并行区域(点(a)(b))不同的地址。但请注意,传递给Base::eval()的临时副本在每个线程中具有相同的地址。我相信这是一个编译器错误,它使得Element的隐式拷贝构造函数使用共享变量。这可以通过查看传递给Base::eval()的地址容易验证 - 它位于地址Nsrc之间,即在共享变量块中。进一步检查汇编源代码可以发现,临时地址的地址确实作为参数传递给函数vcomp100.dll,该函数实现了OpenMP分支/连接模型的分支部分。

由于基本上没有可从启用的优化除了影响此行为导致Base::eval()Base::eval_dispatch()Implementation::eval()全部被内联,因此的elem没有临时副本有史以来,唯一的变通编译器选项,我已经发现有:

1)使Element elem参数Base::eval()的引用:

void eval (int dim, Element& elem, double *res) 
{ 
    eval_dispatch(dim, elem, res, &Derived::eval); // Point (2) 
} 

这确保的elem在IM的funclet的堆栈中的本地副本Evaluator<Implementation>::operator()中的并行区域已通过并且不是共享的临时副本。这将作为另一个临时副本进一步传递给Base::eval_dispatch(),但由于此新临时副本位于Base::eval()的堆栈中,而不是在共享变量块中,因此它保留其正确的值。

2)提供一个明确的拷贝构造函数Element

Element (const Element& e) : i_(e.i_), src_(e.src_) {} 

我建议你用明确的拷贝构造函数去,因为它不需要在源代码中的进一步变化。

显然这种行为也出现在MSVS 2008中。我将不得不检查它是否也出现在MSVS 2012中,并可能用MS提交错误报告。

此错误不会在32位代码中显示,因为那里通过值对象传递的每个值的全部值被推送到调用堆栈上,而不仅仅是指向它的指针。

+0

感谢您的回答。如果我正确地理解了你的想法,你基本上会观察到和我一样的行为,我们可能会得出这样的结论:这是MSVC的一些问题。关于关键区域,我也试过只使用一个OMP线程,并没有发生任何问题。但据我所知,简单的问题没有任何线程安全问题,也看不到任何可能的竞争条件。 – 2012-07-17 20:50:08

+0

是的,基本上问题出在那里,但是我没有观察到前几次失败的断言,因为它不会一直发生。开启优化解决了这个问题。在时间允许的情况下,我仍会尽力达到问题的底部,因为它也可能发生在我们的一些用户身上。 – 2012-07-17 21:00:46

+0

感谢您的更新答案。这是非常丰富和有益的。我想我会用明确的拷贝构造函数去。但是我有点担心是否会有任何性能影响。在实际的程序中,元素被设计得像一个迭代器并且有效地传递值是非常重要的。我有点在快速明智的复制计数 – 2012-07-18 12:25:45