2012-07-13 21 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