2012-07-13 1 views
11

J'ai un programme assez complexe qui fonctionne dans un comportement étrange lors de la construction avec OpenMP en mode débogage MSVC 2010. J'ai fait de mon mieux pour construire l'exemple de travail minimal suivant (bien que ce ne soit pas vraiment minimal), ce qui minimise la structure du vrai programme.OpenMP avec MSVC 2010 Debug construit bug étrange lorsque l'objet est copié

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

Le programme réel ne pas vector etc. Mais le Element, Base, Evaluator et Implementation capture la structure de base du programme réel. Lorsque vous générez en mode débogage et exécutez le débogueur, l'assertion échoue à Point (4).

Voici quelques détails des informations de débogage, en consultant les piles d'appels,

A l'entrée Point (1), la i locale a une valeur 371152, ce qui est bien. La variable elem n'apparaît pas dans le cadre, ce qui est un peu étrange. Mais puisque l'affirmation à Point (1) ne manque pas, je suppose que c'est bien.

Ensuite, des choses folles sont arrivées. L'appel à eval par Evaluator résout à sa classe de base, et donc Point (2) était exectuted. À ce stade, les debugers montre que le elem a i_ = 499999, qui n'est plus le i utilisé pour créer elem dans Evaluator avant de le passer en valeur-Base::eval. Le point suivant, il résout à Point (3), cette fois, elem a i_ = 501682, qui est hors de portée, et c'est la valeur lorsque l'appel est dirigé à Point (4) et a échoué l'assertion.

Il semble que chaque fois que l'objet Element est passé par valeur, la valeur de ses membres est modifiée. Réexécutez le programme plusieurs fois, des comportements similaires se produisent mais pas toujours reproductibles. Dans le programme réel, cette classe est conçue pour aimer un itérateur, qui parcourt une collection de particules. Bien que la chose itérer n'est pas exaclty comme un conteneur. Mais de toute façon, le fait est qu'il est suffisamment petit pour être efficacement passé en valeur. Et par conséquent, le code client, sait qu'il a sa propre copie de Element au lieu d'une référence ou un pointeur, et n'a pas besoin de se soucier de thread-safe (beaucoup) tant qu'il reste avec l'interface de Element, qui fournissent seulement écrire l'accès à une seule position de la collection entière.

J'ai essayé le même programme avec GCC et Intel ICPC. Rien d'inattendu ne se produit. Et dans le vrai programme, corrigez les résultats là où ils sont produits. Ai-je mal utilisé OpenMP quelque part? Je pensais que le elem créé à environ Point (1) doit être local au corps de la boucle. De plus, dans l'ensemble du programme, aucune valeur supérieure à N n'a été produite, alors d'où vient cette nouvelle valeur?

Modifier

je plus attentivement regardé dans le débogueur, il montre que le pointeur elem.src_ ne change pas pendant elem.i_ a été changé quand elem a été adoptée par la valeur, avec elle.Il a la même valeur (de l'adresse de la mémoire) après passé par valeur

Edit: drapeaux du compilateur

je CMake pour générer la solution MSVC. Je dois avouer que je n'ai aucune idée de comment utiliser MSVC ou Windows en général. La seule raison pour laquelle je l'utilise est que je sais que beaucoup de gens l'utilisent donc je veux tester ma bibliothèque pour éviter tout problème.

Le CMake généré projet, en utilisant Visual Studio 10 Win64 cible, les drapeaux du compilateur semble être /DWIN32 /D_WINDOWS /W3 /Zm1000 /EHsc /GR /D_DEBUG /MDd /Zi /Ob0 /Od /RTC1 Et voici la ligne de commande qui se trouve dans la propriété Pages-C/C++ - ligne de commande /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

Y at-il suspecious ici?

+0

Parfois, des choses étranges peuvent se produire lorsqu'un code est compilé en tant que Release et que d'autres sont compilés en tant que Debug. L'OpenMP que vous utilisez est-il compilé avec les mêmes options de drapeaux/déboguage que votre programme? –

+0

Je ne suis pas sûr de la question. Je n'utilise généralement pas msvc sauf pour les tests. Cependant, le code ci-dessus était un programme de fichiers unique. Donc je suppose que quel que soit le drapeau utilisé, il est utilisé pour l'ensemble du programme. Y at-il une option spéciale pour le mode de débogage openmp? J'ai utilisé cmake pour trouver le drapeau openmp, qui devient put/openmp. @SethCarnegie –

+0

compilez-vous OpenMP avec ce fichier ou utilisez-vous une bibliothèque qui a été compilée à un autre moment? –

Répondre

8

Apparemment, l'implémentation OpenMP 64 bits dans MSVC n'est pas compatible avec le code, compilée sans optimisations.

Pour déboguer votre problème, j'ai modifié votre code pour enregistrer le numéro d'itération à une variable globale threadprivate juste avant l'appel à this->eval() puis ajouté un chèque au début de Implementation::eval() pour voir si le nombre d'itérations enregistrée diffère de 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(); 
} 
... 

Il semble que la valeur aléatoire de elem.i_ devient un mauvais mélange des valeurs passées dans les différents threads void eval_dispatch(int dim, Element elem, double *res, void (*) (int, Element, double *)). Cela se produit des centaines de fois dans chaque exécution, mais vous ne le voyez qu'une fois que la valeur elem.i_ devient suffisamment grande pour déclencher l'assertion. Parfois, il arrive que la valeur mélangée ne dépasse pas la taille du conteneur et que le code termine l'exécution sans assertion. Aussi ce que vous voyez lors de la session de débogage après l'affirmation est l'incapacité du débogueur VS pour faire face correctement avec le code multithread :)

Cela se produit uniquement en mode 64 non optimisé bits. Cela ne se produit pas dans le code 32 bits (à la fois de débogage et de publication). Cela ne se produit pas non plus dans le code de version 64 bits , sauf si les optimisations sont désactivées. Il ne se produit pas si l'on met l'appel à this->eval() dans une section critique:

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

mais faire cela annulerait les avantages de OpenMP. Cela montre que quelque chose en aval de la chaîne d'appel est effectuée de manière dangereuse. J'ai examiné le code de l'assemblage mais je n'ai pas trouvé la raison exacte. Je suis vraiment perplexe car MSVC implémente le constructeur de copie implicite de la classe Element en utilisant une simple copie bit à bit (il est même en ligne) et toutes les opérations sont effectuées sur la pile. Cela me rappelle le fait que le compilateur du Sun (maintenant Oracle) insiste sur le fait qu'il devrait augmenter le niveau d'optimisation si l'on autorise le support OpenMP. Malheureusement, la documentation de l'option /openmp dans MSDN ne dit rien sur l'interférence possible qui pourrait provenir du "mauvais" niveau d'optimisation. Cela pourrait aussi être un bug. Je devrais tester avec une autre version de VS si je peux en avoir un.

Éditer: J'ai creusé plus profondément comme promis et exécuter le code dans Intel Parallel Inspector 2011. Il a trouvé un modèle de course de données comme prévu.Apparemment, lorsque cette ligne est exécutée:

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

une copie temporaire de elem est créé et transmis par adresse à la méthode eval() comme cela est requis par Windows x64 ABI. Et voici la chose étrange: l'emplacement de cette copie temporaire n'est pas sur la pile du funclet qui implémente la région parallèle (le compilateur MSVC l'appelle d'ailleurs Evaluator$omp$1<Implementation>::operator()) comme on s'y attendrait mais plutôt son adresse est considérée comme le premier argument de l'amusement. Comme cet argument est une seule et même dans toutes les discussions, cela signifie que la copie temporaire qui est encore passé à this->eval() est en fait partagé entre les threads, ce qui est ridicule, mais est toujours vrai que l'on peut facilement observer:

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

exécution de ce code produit une sortie similaire à ceci:

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

Comme prévu elem a des adresses différentes dans chaque thread d'exécution parallèle de la région (points (a) et (b)). Mais observez que la copie temporaire qui est passée à Base::eval() a la même adresse dans chaque thread. Je crois que c'est un bogue de compilateur qui fait que le constructeur de copie implicite de Element utilise une variable partagée. Cela pourrait être facilement validé en regardant l'adresse passée à Base::eval() - il se situe quelque part entre l'adresse de N et celle de src, c'est-à-dire dans le bloc de variables partagées. Une inspection plus approfondie de la source d'assemblage révèle que l'adresse de la place temporaire est en effet transmise en tant qu'argument à la fonction _vcomp_fork() de vcomp100.dll qui implémente la partie fork du modèle OpenMP fork/join.

Depuis fondamentalement, il n'y a pas d'options de compilateur qui peuvent influencer ce comportement à part permettant Optimisations ce qui conduit à Base::eval(), Base::eval_dispatch() et Implementation::eval() tous étant inline et donc pas de copies temporaires de elem sont jamais fait, le seul contournements que je ont trouvé sont:

1) Faire l'argument Element elem-Base::eval() une référence:

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

Cela garantit que la copie locale de elem dans la pile de funclet qui im La zone parallèle dans Evaluator<Implementation>::operator() est transmise et non la copie temporaire partagée. Cela est également passé en valeur comme une autre copie temporaire à Base::eval_dispatch() mais il conserve sa valeur correcte car cette nouvelle copie temporaire se trouve dans la pile Base::eval() et non dans le bloc de variables partagées.

2) Fournir un constructeur de copie explicite à Element:

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

Je vous recommande d'aller avec le constructeur de copie explicite car il ne nécessite pas d'autres changements dans le code source.

Apparemment, ce comportement est également présent dans MSVS 2008. Je devrais vérifier s'il est également présent dans MSVS 2012 et éventuellement déposer un rapport de bogue avec MS.

Ce bogue n'apparaît pas dans le code 32 bits car la valeur entière de chaque objet transmis par valeur est poussée sur la pile d'appels et pas seulement un pointeur vers elle.

+0

Merci pour la réponse. Si j'ai bien compris votre idée, vous avez essentiellement observé le même comportement que moi et nous pouvons conclure que c'est un problème avec MSVC. En ce qui concerne la région critique, j'ai également essayé d'utiliser un seul thread OMP et aucun problème n'est survenu. Cependant, autant que je sache, le problème simple n'a pas de problème de sécurité des threads, aussi je ne vois pas de condition de concurrence possible. –

+0

Oui, fondamentalement le problème était là mais je n'ai pas observé l'affirmation défaillante les premières fois car cela n'arrive pas tout le temps. Activer les optimisations le résout. J'essaierai toujours d'aller au fond du problème quand le temps le permettra, car cela pourrait aussi arriver à certains de nos utilisateurs. –

+0

Merci pour la nouvelle mise à jour. C'est très instructif et utile. Je pense que je vais aller avec le constructeur de copie explicite. Mais je suis un peu inquiet s'il y aura un impact sur les performances. Dans le programme actuel, l'élément est conçu comme un itérateur et il est essentiel de le passer en valeur efficacement. Et je compte en quelque sorte sur la copie rapide peu sage –