草庐IT

c++ - 复制对象时,带有 MSVC 2010 Debug 的 OpenMP 会生成奇怪的错误

coder 2023-11-13 原文

我有一个相当复杂的程序,在 MSVC 2010 Debug模式下使用 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等等 但是Element , Base , EvaluatorImplementation捕获真实程序的基本结构。在 Debug模式下构建并运行调试器时,断言在 Point (4) 处失败.

这是调试信息的更多详细信息,通过查看调用堆栈,

进入时 Point (1) ,本地i有值 371152 ,这很好。变量 elem没有出现在框架中,这有点奇怪。但由于 Point (1) 处的断言没有失败,我想这很好。

然后,疯狂的事情发生了。调用 eval来自 Evaluator解析为其基类,因此 Point (2)被执行。此时,调试器显示 elemi_ = 499999 ,不再是 i用于创建 elemEvaluator在通过之前按值 Base::eval .下一点,它解析为 Point (3) ,这一次,elemi_ = 501682 ,超出范围,这是调用定向到 Point (4) 时的值并且断言失败。

看起来就像每当 Element对象按值传递,其成员的值发生变化。多次重新运行程序,会发生类似的行为,但并不总是可重现。在实际程序中,这个类被设计为类似于一个迭代器,它对粒子集合进行迭代。虽然它迭代的东西不像容器那样精确。但无论如何,关键是它足够小,可以有效地通过值传递。因此,客户端代码知道它有自己的 Element 拷贝。而不是一些引用或指针,只要他坚持 Element 就不需要担心线程安全(很多)的接口(interface),它只提供对整个集合的单个位置的写访问。

我用 GCC 和 Intel ICPC 尝试了相同的程序。没有任何意外发生。在实际程序中,产生正确的结果。

我是否在某处错误地使用了 OpenMP?我以为elem创建于约 Point (1)应该是循环体的局部。另外,在整个程序中,没有大于N的值。产生了,那么这些新的值(value)是从哪里来的呢?

编辑

我更仔细地查看了调试器,它显示虽然 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这是在 Property Pages-C/C++-Command Line 中找到的命令行/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
这里有什么可疑的吗?

最佳答案

显然,MSVC 中的 64 位 OpenMP 实现与未经优化编译的代码不兼容。

为了调试您的问题,我修改了您的代码以将迭代编号保存到 threadprivate调用 this->eval() 之前的全局变量然后在Implementation::eval()的开头加了一个check查看保存的迭代次数是否与 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_ 的值。变得足够大以触发断言。有时会发生混合值没有超过容器的大小,然后代码在没有断言的情况下完成执行。此外,您在断言后的调试 session 期间看到的是 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);
    }
}

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

这让我想起了一个事实,即 Sun 的(现在是 Oracle 的)编译器坚持认为,如果启用 OpenMP 支持,它应该提高优化级别。不幸的是 /openmp 的文档MSDN 中的选项没有说明可能来自“错误”优化级别的可能相互引用。这也可能是一个错误。如果可以访问,我应该使用另一个版本的 VS 进行测试。

编辑:我按照 promise 进行了更深入的挖掘,并在英特尔 Parallel Inspector 2011 中运行了代码。它发现了一种符合预期的数据竞争模式。显然,当这一行被执行时:
this->eval(dim, elem, r);
elem 的临时拷贝创建并通过地址传递给 eval() Windows x64 ABI 要求的方法。奇怪的事情来了:这个临时拷贝的位置不在实现并行区域的 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);
    }
}
...

运行此代码会产生类似于以下内容的输出:
[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() 的地址轻松验证。 - 它位于 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)
}

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

2) 为 Element 提供显式复制构造函数:
Element (const Element& e) : i_(e.i_), src_(e.src_) {}

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

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

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

关于c++ - 复制对象时,带有 MSVC 2010 Debug 的 OpenMP 会生成奇怪的错误,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/11479337/

有关c++ - 复制对象时,带有 MSVC 2010 Debug 的 OpenMP 会生成奇怪的错误的更多相关文章

  1. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  2. ruby-on-rails - 按天对 Mongoid 对象进行分组 - 2

    在控制台中反复尝试之后,我想到了这种方法,可以按发生日期对类似activerecord的(Mongoid)对象进行分组。我不确定这是完成此任务的最佳方法,但它确实有效。有没有人有更好的建议,或者这是一个很好的方法?#eventsisanarrayofactiverecord-likeobjectsthatincludeatimeattributeevents.map{|event|#converteventsarrayintoanarrayofhasheswiththedayofthemonthandtheevent{:number=>event.time.day,:event=>ev

  3. ruby-on-rails - 如何优雅地重启 thin + nginx? - 2

    我的瘦服务器配置了nginx,我的ROR应用程序正在它们上运行。在我发布代码更新时运行thinrestart会给我的应用程序带来一些停机时间。我试图弄清楚如何优雅地重启正在运行的Thin实例,但找不到好的解决方案。有没有人能做到这一点? 最佳答案 #Restartjustthethinserverdescribedbythatconfigsudothin-C/etc/thin/mysite.ymlrestartNginx将继续运行并代理请求。如果您将Nginx设置为使用多个上游服务器,例如server{listen80;server

  4. ruby-on-rails - 如何验证非模型(甚至非对象)字段 - 2

    我有一个表单,其中有很多字段取自数组(而不是模型或对象)。我如何验证这些字段的存在?solve_problem_pathdo|f|%>... 最佳答案 创建一个简单的类来包装请求参数并使用ActiveModel::Validations。#definedsomewhere,atthesimplest:require'ostruct'classSolvetrue#youcouldevencheckthesolutionwithavalidatorvalidatedoerrors.add(:base,"WRONG!!!")unlesss

  5. Ruby 写入和读取对象到文件 - 2

    好的,所以我的目标是轻松地将一些数据保存到磁盘以备后用。您如何简单地写入然后读取一个对象?所以如果我有一个简单的类classCattr_accessor:a,:bdefinitialize(a,b)@a,@b=a,bendend所以如果我从中非常快地制作一个objobj=C.new("foo","bar")#justgaveitsomerandomvalues然后我可以把它变成一个kindaidstring=obj.to_s#whichreturns""我终于可以将此字符串打印到文件或其他内容中。我的问题是,我该如何再次将这个id变回一个对象?我知道我可以自己挑选信息并制作一个接受该信

  6. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

    如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

  7. ruby-on-rails - 未在 Ruby 中初始化的对象 - 2

    我在Rails工作并有以下类(class):classPlayer当我运行时bundleexecrailsconsole然后尝试:a=Player.new("me",5.0,"UCLA")我回来了:=>#我不知道为什么Player对象不会在这里初始化。关于可能导致此问题的操作/解释的任何建议?谢谢,马里奥格 最佳答案 havenoideawhythePlayerobjectwouldn'tbeinitializedhere它没有初始化很简单,因为你还没有初始化它!您已经覆盖了ActiveRecord::Base初始化方法,但您没有调

  8. ruby - 如何在 Rails 4 中使用表单对象之前的验证回调? - 2

    我有一个服务模型/表及其注册表。在表单中,我几乎拥有服务的所有字段,但我想在验证服务对象之前自动设置其中一些值。示例:--服务Controller#创建Action:defcreate@service=Service.new@service_form=ServiceFormObject.new(@service)@service_form.validate(params[:service_form_object])and@service_form.saverespond_with(@service_form,location:admin_services_path)end在验证@ser

  9. ruby - 一个 YAML 对象可以引用另一个吗? - 2

    我想让一个yaml对象引用另一个,如下所示:intro:"Hello,dearuser."registration:$introThanksforregistering!new_message:$introYouhaveanewmessage!上面的语法只是它如何工作的一个例子(这也是它在thiscpanmodule中的工作方式。)我正在使用标准的ruby​​yaml解析器。这可能吗? 最佳答案 一些yaml对象确实引用了其他对象:irb>require'yaml'#=>trueirb>str="hello"#=>"hello"ir

  10. ruby - 更改 ActiveRecord 中对象的类 - 2

    假设我有一个FireNinja我的数据库中的对象,使用单表继承存储。后来才知道他真的是WaterNinja.将他更改为不同的子类的最干净的方法是什么?更好的是,我很想创建一个新的WaterNinja对象并替换旧的FireNinja在数据库中,保留ID。编辑我知道如何创建新的WaterNinja来self现有FireNinja的对象,我也知道我可以删除旧的并保存新的。我想做的是改变现有项目的类别。我是通过创建一个新对象并执行一些ActiveRecord魔法来替换行,还是通过对对象本身做一些疯狂的事情,或者甚至通过删除它并使用相同的ID重新插入来做到这一点,这是问题的一部分。

随机推荐