2012-07-13 6 views
11

Mam dość złożony program, który działa dziwnie podczas kompilacji z OpenMP w trybie debugowania MSVC 2010. Zrobiłem co w mojej mocy, aby stworzyć następujący minimalny przykład pracy (choć nie jest to naprawdę minimalny), który minicuje strukturę prawdziwego programu.OpenMP z MSVC 2010 Debug buduje dziwny błąd, gdy obiekt jest kopiowany

#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; 
} 

Prawdziwy program nie ma vector itp ale Element, Base, Evaluator i Implementation oddaje podstawową strukturę prawdziwego programu. Podczas kompilacji w trybie debugowania i uruchamiania debuggera asercja kończy się niepowodzeniem na Point (4).

Oto kilka szczegółów informacje debugowania, przeglądając stosy połączeń,

Przy wejściu Point (1) lokalna i ma wartość 371152, co jest w porządku. Zmienna elem nie pojawia się w ramce, co jest trochę dziwne. Ale ponieważ twierdzenie o Point (1) nie zawodzi, myślę, że jest w porządku.

Potem wydarzyły się zwariowane rzeczy. Wywołanie eval przez Evaluator rozwiązuje swoją klasę podstawową, a więc Point (2) zostało exectute. W tym momencie, debugers pokazuje, że elem ma i_ = 499999, który nie jest już w i użyte do wygenerowania elem w Evaluator przed przekazaniem go wartością do Base::eval jest. Następnym punktem jest Point (3), tym razem elem ma i_ = 501682, który jest poza zakresem i jest to wartość, gdy połączenie jest kierowane do Point (4) i nie powiodło się potwierdzenie.

Wygląda na to, że gdy obiekt zostanie przekazany przez wartość, wartość jego członków zostanie zmieniona. Ponownie uruchom program wiele razy, podobne zachowania zachodzą, ale nie zawsze są powtarzalne. W prawdziwym programie, ta klasa jest zaprojektowana tak, aby lubić iterator, który iteruje po zbiorze cząsteczek. Chociaż to, co iteruje, nie jest exaclty jak pojemnik. W każdym razie chodzi o to, że jest wystarczająco mały, aby skutecznie przejść przez wartość. Dlatego też kod klienta wie, że ma swoją własną kopię Element zamiast jakiegoś odniesienia lub wskaźnika, i nie musi martwić się o wątek (dużo), dopóki trzyma się z interfejsem Element, który zapewnia tylko zapisz dostęp do pojedynczej pozycji całej kolekcji.

Próbowałem tego samego programu z GCC i Intel ICPC. Nie dzieje się nic nieoczekiwanego. I w prawdziwym programie, poprawne wyniki tam, gdzie zostały wyprodukowane.

Czy nie używałem OpenMP w niewłaściwy sposób? Myślałem, że elem utworzony na około Point (1) będzie lokalny dla ciała pętli. Ponadto w całym programie nie powstała wartość większa niż N, skąd więc pochodzi ta nowa wartość?

Edit

spojrzałem dokładniej do debuggera, to pokazuje, że podczas gdy elem.i_ została zmieniona podczas elem został przekazany przez wartość, wskaźnik elem.src_ nie zmienia się z nim.Ma taką samą wartość (adresu pamięci) po przekazywane przez wartość

Edit: Compiler flagi

użyłem CMake do generowania rozwiązanie MSVC. Muszę przyznać, że nie mam pojęcia, jak korzystać z MSVC lub systemu Windows w ogóle. Jedynym powodem, dla którego go używam, jest to, że wiem, że wiele osób go używa, więc chcę przetestować moją bibliotekę, aby obejść jakiekolwiek problemy.

CUpewnij wygenerowany projekt, używając Visual Studio 10 Win64 cel, flagi kompilatora wydaje się być /DWIN32 /D_WINDOWS /W3 /Zm1000 /EHsc /GR /D_DEBUG /MDd /Zi /Ob0 /Od /RTC1 A oto wiersz polecenia znalezione w 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

Czy jest coś suspecious tutaj?

+0

Czasami dziwne rzeczy mogą się zdarzyć, gdy niektóre kod jest kompilowany jako Uwalniania i niektórych jest kompilowany jako debugowania. Czy OpenMP, którego używasz, jest skompilowany z tymi samymi flagami/debugowaniem, co twój program? –

+0

Nie jestem pewien co do tego pytania. Zwykle nie używam msvc z wyjątkiem testów. Jednak powyższy kod był pojedynczym programem plików. Więc domyślam się, że jakakolwiek flaga jest używana, jest używana dla całego programu. Czy istnieje specjalna opcja dla trybu debugowania openmp? Użyłem cmake, aby znaleźć flagę openmp, która zmienia się w put/be/openmp. @SethCarnegie –

+0

czy kompilujesz OpenMP z tym plikiem, czy używasz biblioteki, która została skompilowana w innym czasie? –

Odpowiedz

8

Wygląda na to, że 64-bitowa implementacja OpenMP w MSVC nie jest zgodna z kodem, skompilowana bez optymalizacji.

Aby debugować problem, mam zmodyfikowany kod, aby zapisać numer iteracji do zmiennej globalnej threadprivate tuż przed wywołaniem this->eval() i dodaje czek na początku Implementation::eval() aby sprawdzić, czy zapisany numer iteracji różni się od 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(); 
} 
... 

wydaje się, że losowo wartość elem.i_ staje się zła mieszanka wartości przekazywane w różnych wątków void eval_dispatch(int dim, Element elem, double *res, void (*) (int, Element, double *)). Zdarza się to często w każdym cyklu, ale widzisz go tylko wtedy, gdy wartość elem.i_ staje się wystarczająco duża, aby wywołać asercję. Czasami zdarza się, że wartość mieszana nie przekracza rozmiaru kontenera, a następnie kod kończy wykonywanie bez zapewnienia. Również to, co można zobaczyć w trakcie sesji debugowania po stwierdzeniu jest niezdolność debugger VS prawidłowo radzić sobie z kodem wielowątkowym :)

Dzieje się tak tylko w trybie 64-bitowym nieoptymalizowane. Nie dzieje się to w 32-bitowym kodzie (zarówno debugowaniu, jak i wydaniu). Nie dzieje się tak również w 64-bitowym kodzie zwalniającym , chyba że optymalizacje są wyłączone. To również nie stanie, jeśli ktoś stawia wezwanie do this->eval() w krytycznym punkcie:

#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); 
    } 
} 

ale robi to byłoby anulować korzyści OpenMP. Pokazuje to, że coś dalej w łańcuchu połączeń jest wykonywane w niebezpieczny sposób. Sprawdziłem kod zespołu, ale nie mogłem znaleźć dokładnego powodu. Jestem naprawdę zaintrygowany, ponieważ MSVC implementuje niejawny konstruktor kopii klasy Element przy użyciu prostej kopii bitowej (jest to nawet inline) i wszystkie operacje są wykonywane na stosie.

To przypomina mi o tym, że kompilator firmy Sun (obecnie Oracle) twierdzi, że powinien on podnieść poziom optymalizacji, jeśli umożliwi obsługę OpenMP. Niestety, dokumentacja opcji /openmp w MSDN nie mówi nic o możliwej interferencji, która może pochodzić z "niewłaściwego" poziomu optymalizacji. To również może być błąd. Powinienem przetestować inną wersję VS, jeśli mogę uzyskać do niej dostęp.

Edit: kopany głębiej jak obiecał i uruchomić kod w Intel Parallel Inspector 2011. Stwierdzono jeden wzór wyścigowego dane zgodnie z oczekiwaniami.Podobno, gdy linia ta jest wykonywana:

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

tymczasowa kopia elem jest tworzone i przekazywane przez adres metody eval() jak jest to wymagane przez Windows x64 ABI. I tu pojawia się dziwna rzecz: lokalizacja tej tymczasowej kopii nie znajduje się na stosie funcletu, który implementuje region równoległy (tak na marginesie) kompilator MSVC nazywa go Evaluator$omp$1<Implementation>::operator()), jak można by oczekiwać, ale raczej jego adres jest brany jako pierwszy argument z funcletu. Jako argument ten jest jeden i ten sam we wszystkich wątków, oznacza to, że tymczasowa kopia, która zostanie dalej przekazywane do this->eval() jest rzeczywiście dzielone pomiędzy wszystkich wątków, co jest śmieszne, ale nadal jest prawdą, jak można łatwo zaobserwować:

... 
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); 
    } 
} 
... 

Uruchomienie tego kodu generuje wynik podobny do tego:

[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 <---- !! 

jak planowana elem ma różnych miejscach w każdej nici realizujący obszar (równolegle punktów (a) i (b)). Ale zauważ, że tymczasowa kopia, która zostanie przekazana do Base::eval(), ma ten sam adres w każdym wątku. Uważam, że jest to błąd kompilatora, który powoduje, że domyślny konstruktor kopiowania z Element używa zmiennej współdzielonej udostępnionej. Można to łatwo zweryfikować, patrząc na adres przekazany do adresu Base::eval() - znajduje się on gdzieś pomiędzy adresem N i adresem src, tj. W bloku współdzielonych zmiennych. Dalsza inspekcja źródła zespołu ujawnia, że ​​rzeczywiście adres tymczasowego miejsca jest przekazywany jako argument do funkcji _vcomp_fork() od vcomp100.dll, która implementuje część widelkową modelu fork/join OpenMP.

Ponieważ w zasadzie nie istnieją opcje kompilatora, które mogą mieć wpływ na to zachowanie oprócz umożliwiające optymalizacje, które prowadzi do Base::eval(), Base::eval_dispatch() i Implementation::eval() wszystko jest inlined i stąd żadne tymczasowe kopie elem są zawsze wykonane, jedyne obejścia że znalazłem to:

1) Dokonać Element elem argument Base::eval() odniesienie:

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

gwarantuje to, że lokalna kopia elem w stosie z funclet że im Plany przekazuje się region równoległy pod numerem Evaluator<Implementation>::operator(), a nie udostępnioną kopię tymczasową. Ta wartość jest dodatkowo przekazywana jako wartość innej tymczasowej kopii do Base::eval_dispatch(), ale zachowuje ona poprawną wartość, ponieważ ta nowa kopia tymczasowa znajduje się w stosie Base::eval(), a nie w bloku współdzielonych zmiennych.

2) Zapewnienie wyraźnej kopii konstruktora Element:

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

Polecam że idziesz za wyraźną kopię konstruktora, gdyż nie wymaga dalszych zmian w kodzie źródłowym.

Podobno to zachowanie występuje również w MSVS 2008. Musiałbym sprawdzić, czy jest on obecny również w MSVS 2012 i ewentualnie zgłosić raport o błędach z MS.

Błąd ten nie jest wyświetlany w 32-bitowym kodzie, ponieważ cała wartość każdego przekazanego przez obiekt wartości jest przekazywana na stos wywoławczy, a nie tylko wskaźnik do niego.

+0

Dzięki za odpowiedź. Jeśli dobrze zrozumiałem Twój pomysł, zasadniczo zaobserwowałeś to samo co ja i możemy wywnioskować, że jest to pewien problem z MSVC. Jeśli chodzi o krytyczny region, spróbowałem użyć tylko jednego wątku OMP i nie wystąpiły żadne problemy. Jednak, o ile mogę powiedzieć, prosty problem nie ma żadnego problemu z bezpieczeństwem gwintu, także nie widzę żadnego możliwego stanu wyścigu. –

+0

Tak, w zasadzie problem istniał, ale pierwsze kilka razy nie zaobserwowałem stwierdzenia błędu, jako że nie stało się to przez cały czas. Włączenie optymalizacji rozwiązuje problem. Nadal będę starał się dotrzeć do sedna problemu, gdy czas na to pozwoli, ponieważ może się to również zdarzyć niektórym z naszych użytkowników. –

+0

Dziękujemy za zaktualizowaną odpowiedź. Jest bardzo pouczający i pomocny. Myślę, że pójdę z konstruktorem jawnej kopii. Ale jestem trochę zaniepokojony, jeśli wystąpi jakikolwiek wpływ na wydajność. W rzeczywistym programie Element jest zaprojektowany jako iterator, a przekazanie go według wartości jest sprawą zasadniczą. I w pewnym sensie liczę na szybką, bitową kopię: –

Powiązane problemy