OpenMP with MSVC 2010 Debug在复制对象时构建奇怪的bug

Yan*_*hou 11 c++ openmp visual-c++

我有一个相当复杂的程序,在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;
}
Run Code Online (Sandbox Code Playgroud)

真正的程序没有vector等.但Element,Base,EvaluatorImplementation捕捉真正的程序的基本结构.在调试模式下构建并运行调试器时,断言失败Point (4).

以下是调试信息的更多细节,通过查看调用堆栈,

进入时Point (1),当地i有价值371152,这很好.变量elem没有显示在框架中,这有点奇怪.但由于断言Point (1)不会失败,我想这很好.

然后,疯狂的事情发生了.eval通过Evaluator解析它的基类调用,所以Point (2)被解释了.在这一点上,debugers表明elemi_ = 499999,这不再是i用来创建elemEvaluator传递之前,将通过价值Base::eval.第二点,它解析为Point (3),这个时候,elemi_ = 501682,这是超出范围,这是当呼叫指向值Point (4)和失败的断言.

看起来每当Element对象按值传递时,其成员的值都会更改.多次重新运行程序,虽然并不总是可重现,但会发生类似的行为.在真正的程序中,这个类被设计成像迭代器一样,迭代一组粒子.虽然它迭代的东西不像容器那样exaclty.但无论如何,重点是它足够小,可以通过价值有效地传递.因此,客户端代码知道它有自己的副本Element而不是一些引用或指针,并且只要他坚持使用Element只提供写访问权限的接口,就不需要担心线程安全(多)到整个系列的单一位置.

我尝试了与GCC和英特尔ICPC相同的程序.没有任何不期待的事情发生.在真实的程序中,产生正确的结果.

我在某处错误地使用过OpenMP吗?我认为elem在about处创建的Point (1)应该是循环体的局部.此外,在整个计划中,没有比N生产的更大的价值,那么这些新价值来自何处?

编辑

我仔细看了一下调试器,它表明虽然elem.i_elem按值传递时被更改,但指针elem.src_不会随之改变.它通过值后具有相同的值(内存地址)

编辑:编译器标志

我使用CMake生成MSVC解决方案.我必须承认我一般不知道如何使用MSVC或Windows.我使用它的唯一原因是我知道很多人都使用它,所以我想测试我的库以解决任何问题.

CMake生成的项目,使用Visual Studio 10 Win64target,编译器标志似乎是 /DWIN32 /D_WINDOWS /W3 /Zm1000 /EHsc /GR /D_DEBUG /MDd /Zi /Ob0 /Od /RTC1.这里是Property Pages-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

这里有什么可疑的吗?

Hri*_*iev 8

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

为了调试你的问题,我修改了你的代码,以便threadprivate在调用之前将迭代次数保存到一个全局变量this->eval(),然后在开头添加一个检查,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();
}
...
Run Code Online (Sandbox Code Playgroud)

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

这仅发生在未经优化的64位模式中.它不会发生在32位代码中(包括调试和发布).除非禁用优化,否则在64位版本代码中也不会发生这种情况.如果将呼叫this->eval()置于关键部分,也不会发生这种情况:

#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);
    }
}
Run Code Online (Sandbox Code Playgroud)

但这样做会取消OpenMP的好处.这表明调用链中的某些内容以不安全的方式执行.我检查了汇编代码,但找不到确切的原因.我真的很困惑,因为MSVC Element使用简单的按位复制(甚至是内联)实现类的隐式复制构造函数,并且所有操作都在堆栈上完成.

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

编辑:我按照承诺深入挖掘并运行英特尔Parallel Inspector 2011中的代码.它找到了一个数据竞争模式.显然,当执行此行时:

this->eval(dim, elem, r);
Run Code Online (Sandbox Code Playgroud)

根据Windows x64 ABI的要求,elem创建临时副本并按地址传递给eval()方法.这里有一个奇怪的事情:这个临时副本的位置不在实现并行区域的funclet的堆栈上(MSVC编译器Evaluator$omp$1<Implementation>::operator()按照预期的方式调用它),而是将其地址作为第一个参数. 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);
    }
}
...
Run Code Online (Sandbox Code Playgroud)

运行此代码会产生类似于此的输出:

[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 <---- !!
Run Code Online (Sandbox Code Playgroud)

正如所料,elem在执行并行区域的每个线程中有不同的地址(点(a)(b)).但请注意,传递给的临时副本Base::eval()在每个线程中具有相同的地址.我相信这是一个编译器错误,使隐式复制构造函数Element使用共享变量.这可以通过查看传递给Base::eval()它的地址轻松验证- 它位于地址N和地址之间src,即在共享变量块中.对汇编源的进一步检查表明,临时场所的地址确实作为参数传递给_vcomp_fork()函数,该函数vcomp100.dll实现了OpenMP fork/join模型的fork部分.

由于基本上没有可从启用导致的优化除了影响这种行为的编译器选项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)
}
Run Code Online (Sandbox Code Playgroud)

这可确保传递elem实现并行区域的funclet堆栈中的本地副本,Evaluator<Implementation>::operator()而不是共享临时副本.这通过值进一步传递为另一个临时副本,Base::eval_dispatch()但它保留了正确的值,因为这个新的临时副本位于堆栈中,Base::eval()而不是在共享变量块中.

2)提供一个显式的复制构造函数Element:

Element (const Element& e) : i_(e.i_), src_(e.src_) {}
Run Code Online (Sandbox Code Playgroud)

我建议您使用显式复制构造函数,因为它不需要在源代码中进一步更改.

显然,这种行为也存在于MSVS 2008中.我必须检查它是否也出现在MSVS 2012中,并可能向MS提交错误报告.

这个错误没有在32位代码中显示,因为值对象传递的每个值的整个值都被推送到调用堆栈而不仅仅是指向它的指针.