IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

  1. #1
    Nouveau membre du Club
    Homme Profil pro
    Étudiant
    Inscrit en
    Octobre 2018
    Messages
    28
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 26
    Localisation : France, Isère (Rhône Alpes)

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Octobre 2018
    Messages : 28
    Points : 30
    Points
    30
    Par défaut Baisse de performances après redéfinition d'une méthode dans la classe fille, en dupliquant le code de la mère
    Bonjour,

    Je travaille sur un code de simulation ne se composant que d'une seule classe Model responsable d'à peu près tout les aspects de la simulation.
    J'ai entrepris d'implémenter une forme d'héritage car je dois adapter le code à un second type de simulation qui est assez similaire au premier pour en reprendre une bonne partie.

    Ma logique est la suivante : il y a des méthodes et des attributs communs aux deux simulations, ils restent dans la classe de base Model (abstraite), les attributs et méthodes spécifiques aux deux simulations se retrouvent dans deux autres classes qui héritent de Model.

    J'ai fait une première implémentation avec une classe Model et ModelFille, mon objectif étant de reproduire le fonctionnement actuel avant d'aller plus loin (voir à la fin les deux classes que je voulais implémenter).
    J'ai noté un perte de performance de 10%, ce qui est problématique pour moi car la rapidité d’exécution du code est un des sujets centraux de mon projet.

    Afin d'identifier le problème, j'ai découpé mes modifications et je les ai testé à la suite jusqu'à voir une dégradation du temps d’exécution.
    Je tiens à préciser que j'ai relancé ces tests plusieurs fois et qu'à chaque fois j'obtenais sensiblement les mêmes durées d’exécutions.

    Mon point de départ est l'unique classe Model :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Model {
        Model()
        ~Model()
     
        void run();   // Appel runstep dans une boucle sur n steps et output les résultats à une fréquence donnée
        void runstep();   // Fait avancer la simulation d'un step
     
        void methodsCommunes();    // Plusieurs methodes
        void methodsSimu1()
     
        type attributsCommuns_;
        type attributsSimus1_;
    }
    Dans mon main, j'instancie un objet Model et j'appelle run() dessus.

    Ma première modification a été de créer une classe ModelFille vide qui hérite seulement de Model. Son constructeur appelle celui de Model et ne fait rien d'autre, un objet Model et un objet ModelFille devraient donc être équivalents.
    Cependant, si au lieu d’instancier un objet Model, j'instancie un objet ModelFille dans le main, j'ai un grain d'environ 5%. Et je le rappelle, j'obtiens ce résultat quand je relance plusieurs fois ces deux cas, celui où j'instancie ModelFille est toujours plus rapide d'environ 5%.

    J'avance plus loin dans mes modifications. J'ai rajouté à la classe Model une méthode runstepChaleur() qui contient simplement un bout de runstep(). Toutes les autres parties de runstep() sont communes aux deux simulations mais la partie "chaleur" sera différente. J'ai donc besoin de cette méthode runstepChaleur() qui est appelée en fin de runstep() et qui finira par être virtuelle pure.

    Ma classe fille s'est remplie, elle redéfinit maintenant run() et runstep(). Le contenu de ces deux méthodes et identique à celui de Model::run() et Model::runstep().
    La classe ModelFille n'a toujours pas d'attributs supplémentaires et la classe Model contient toujours run() et runstep().
    Quand j'appelle run sur mon objet ModelFille (dans le main), c'est la suite d'appel suivante qui se produit : ModelFillle::run -> ModelFillle::runstep -> Model::runstepChaleur().
    Ici j'ai à peu près les mêmes performances qu'après ma première modification, à savoir environ 5% plus rapide que le cas de départ.

    Ce qui va dégrader mes performances est la modification suivante : dans la classe ModelFille, je redéfinie maintenant la méthode runstepChaleur().
    Maintenant, quand j'appelle run() dans le main, il se passe : ModelFillle::run -> ModelFillle::runstep -> ModelFillle::runstepChaleur().
    Cela est responsable d'une perte de performance de l'ordre de 20% (par rapport au cas initial).

    Dans le cas final, je retombe bizarrement à "seulement" une perte de 10% une fois que je rends la classe Model abstraite et que je défini run et runstepChaleur comme virtuelles pures.

    Y a t'il une explications pour ce gain, et surtout pour cette perte de performance ? Est ce que quelqu'un a déjà rencontré ces effets ?
    Initialement, je l'avais attribué à l'utilisation de méthodes virtuelles mais la plus grosse perte se produit AVANT que je ne passe de méthodes en virtuelle.
    Le problème semble venir du moment où ModelFille::runstep() appelle ModelFillle::runstepChaleur() à la place de Model::runstepChaleur()

    Quelques détails pour aider à la compréhension :
    - dans mes exemples, run() appelle runstep() 1 million de fois et runstep appelle runstepChaleur() une seule fois par appel
    - runstep et runstepChaleur sont de "grosses" méthodes qui prenne un certain temps à s'éxecuter
    - si je désactive le calcul de la chaleur, mon cas de base (1 seule classe) et mon cas final (1 classe abstraite et une classe fille) s’exécutent à peu près à la même vitesse
    - j'utilise OpenMP dans runstep et dans runstepChaleur comme suit, mais quand je désactive OpenMP à la compilation l'effet est toujours présent.
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
     void runstep() {
        #pragma omp parallel
        {
            ...
     
            runstepChaleur();
        }
    }
     
    void runstepChaleur() {
        # pragma omp for
        for (...)
    }
    A la fin, j'aimerais que mes deux classes soient de la forme :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Model {
        Model()
        virtual ~Model()
     
        virtual void run() = 0;
        void runstep();
        virtual void runstep() = 0;
     
        void methodsCommunes();    // Plusieurs methodes
     
        type attributsCommuns_;
    }
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class ModelFille : public Model {
        ModelFille ()
        virtual ~ModelFille ()
     
        virtual void run();
        virtual void runstepChaleur();
     
        void methodsSimu1()
     
        type attributsSimus1_;
    }
    Désolé de la longueur du post, j'ai essayé d'aller à l'essentiel tout en donnant assez de détail.

    Merci d'avance !

  2. #2
    Expert éminent sénior
    Avatar de Kannagi
    Homme Profil pro
    cyber-paléontologue
    Inscrit en
    Mai 2010
    Messages
    3 215
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 35
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : cyber-paléontologue

    Informations forums :
    Inscription : Mai 2010
    Messages : 3 215
    Points : 10 140
    Points
    10 140
    Par défaut
    Personnellement , ton soucis de perf et ce que tu veux "pointer" n'a pas de sens. (de mon point de vue).
    Normalement pour avoir une dif de 20% de perf, c'est que ça soit dans une boucle critique , et je vois aucune boucle là
    Et autre chose : tu as mis les bonnes option du compilo ? (genre -O3 ? ).

    90% de l'optimisation ,c'est souvent optimiser des boucles , changer d'algo , mettre les bonnes options du compilo (qu'on peut "customiser" pour chaque fonction ,vu que des fois du -O2 fait mieux que du -O3).

    C'est rarement en bidouillant du code en espérant que ça fait mieux , surtout sur du code qui semble pas être appelé très souvent là , vu le nom de tes méthodes.

  3. #3
    Nouveau membre du Club
    Homme Profil pro
    Étudiant
    Inscrit en
    Octobre 2018
    Messages
    28
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 26
    Localisation : France, Isère (Rhône Alpes)

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Octobre 2018
    Messages : 28
    Points : 30
    Points
    30
    Par défaut
    Oui je ne suis pas rentré trop dans les détails mais j'ai de grosses boucles for un peu partout (400x600).
    Dans runstepChaleur, j'en ai notamment une qui appelle d'autres méthodes (que j'ai appelé methodsCommunes() dans mon exemple). La structure ressemble à ça :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void runstepChaleur() {
        # pragma omp for
        for (600) {
            for (400) {
                 k1 = methodsCommunes1()
                 k2 = methodsCommunes2()
                 ...
            }
        }
        ...
    }
    J'ai continué de creuser ce matin et je me suis rendu compte que quand runstepChaleur() n'est définie que dans Model, methodsCommunes1 et methodsCommunes2 "ne coûtent rien" alors que quand je la redéfinis dans ModelFille, ces deux méthodes correspondent respectivement à 11% et 3% de l'exécution totale (outil perf sous Linux).

    Quand je les inline dans le header de la classe Model, je retombe sur les "bonnes" performances.
    De ce que j'en déduis, quand j'avais runstepChaleur uniquement dans Model, le compilateur devait faire un inline sans que je le demande. Une fois que je passais runstepChaleur dans ModelFille il ne le faisait plus (je sais qu'inline ne force pas le compilateur et que c'est juste une indication).
    Le mystère semble résolu !

    Par contre, je ne comprends toujours pas le gain quand j'instancie un ModelFille à la place de Model au tout début.

    En tout cas, merci pour la réponse et les quelques conseils.

  4. #4
    Modérateur

    Avatar de Bktero
    Homme Profil pro
    Développeur en systèmes embarqués
    Inscrit en
    Juin 2009
    Messages
    4 481
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 36
    Localisation : France, Loire Atlantique (Pays de la Loire)

    Informations professionnelles :
    Activité : Développeur en systèmes embarqués

    Informations forums :
    Inscription : Juin 2009
    Messages : 4 481
    Points : 13 678
    Points
    13 678
    Billets dans le blog
    1
    Par défaut
    Sais-tu comment fonctionne un appel de fonction virtuelle, qui est à la base du polymorphisme ? As-tu connaissance de ce qu'est une vtable et de leurs possibles impacts sur les performances ?

  5. #5
    Nouveau membre du Club
    Homme Profil pro
    Étudiant
    Inscrit en
    Octobre 2018
    Messages
    28
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 26
    Localisation : France, Isère (Rhône Alpes)

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Octobre 2018
    Messages : 28
    Points : 30
    Points
    30
    Par défaut
    Alors je vais peut être un peu trop simplifier ou pas utiliser la bonne terminologie mais à partir du moment ou il y a du polymorphisme, les fonctions virtuelles sont accédées via une vtable durant l’exécution.

    Après, dans mon cas, je déclare des ModelFille et je ne me sers pas de polymorphisme (dans le sens où je n'utilise pas un pointeur de Model vers un objet ModelFille).
    Donc normalement, le compilateur devrait être capable de déterminer, à la compilation, quelles fonctions appeler (mais je n'en suis pas sûr).

    Après comme je l'expliquais, la perte évoquée dans mon premier message n'était normalement pas imputable au polymorphisme car je n'avais pas de fonctions virtuelles, juste de la redéfinition de méthodes.

  6. #6
    Modérateur

    Avatar de Bktero
    Homme Profil pro
    Développeur en systèmes embarqués
    Inscrit en
    Juin 2009
    Messages
    4 481
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 36
    Localisation : France, Loire Atlantique (Pays de la Loire)

    Informations professionnelles :
    Activité : Développeur en systèmes embarqués

    Informations forums :
    Inscription : Juin 2009
    Messages : 4 481
    Points : 13 678
    Points
    13 678
    Billets dans le blog
    1
    Par défaut
    Tu sembles en effet connaitre les principes de base. Je voulais en être sûr, ça simplifie les réponses qu'on peut t'apporter

    Il y a juste un point qui me semble étrange :

    je n'avais pas de fonctions virtuelles, juste de la redéfinition de méthodes
    Pour redéfinir une fonction membre, elle doit être virtuelle. Si elle n'est pas virtuelle, on ne parle pas de redéfinition mais plutôt de masquage. C'est en général considéré comme une mauvaise pratique.

    je déclare des ModelFille et je ne me sers pas de polymorphisme (dans le sens où je n'utilise pas un pointeur de Model vers un objet ModelFille).
    Donc normalement, le compilateur devrait être capable de déterminer, à la compilation, quelles fonctions appeler (mais je n'en suis pas sûr).
    Ca me semble correct.

    Quand je les inline dans le header de la classe Model, je retombe sur les "bonnes" performances.
    De ce que j'en déduis, quand j'avais runstepChaleur uniquement dans Model, le compilateur devait faire un inline sans que je le demande. Une fois que je passais runstepChaleur dans ModelFille il ne le faisait plus (je sais qu'inline ne force pas le compilateur et que c'est juste une indication).
    L'inlining peut faire des grosses différences sur les performances (à la hausse comme à la baisse, c'est pour ça qu'il vaut mieux laisser le compilateur décider). Avec de la virtualité, je pense qu'il est plus difficile pour le compilateur de faire de l'inlining car il ne peut pas vraiment savoir quelle implémentation va être appelée.

  7. #7
    Nouveau membre du Club
    Homme Profil pro
    Étudiant
    Inscrit en
    Octobre 2018
    Messages
    28
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 26
    Localisation : France, Isère (Rhône Alpes)

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Octobre 2018
    Messages : 28
    Points : 30
    Points
    30
    Par défaut
    Pour redéfinir une fonction membre, elle doit être virtuelle. Si elle n'est pas virtuelle, on ne parle pas de redéfinition mais plutôt de masquage.
    Je ne connaissais pas le terme de masquage mais en effet c'est ce que j'avais fait comme étape intermédiaire afin de trouver quelle "petite" modification était responsable de la baisse de performance.
    Le code final n'en contient pas.

    C'est en général considéré comme une mauvaise pratique.
    Je suis intrigué par ce point, c'est la première fois que j'entends que c'est une mauvaise pratique.
    Dans mon cas je ne l'utilise pas car conceptuellement ça ne fait pas de sens d'avoir une implémentation de ces méthodes dans la classe Model, mais dans d'autres cas j'aurais pu être amené à le faire.

    Imaginons que j'ai une classe de base (non abstraite) A et une classe B qui en hérite et qui ajoute un attribut.
    Dans la classe A, une méthode affiche() affiche tous les attributs dans la console. Dans la classe B, j'aimerais faire de même en ajoutant l'attribut supplémentaire à l'affichage.
    Dans ce cas là, j'aurais sûrement masqué ou redéfini la méthode dans B, et si je n'avais pas prévu d'utiliser de polymorphisme, j'aurais eu tendance à ne pas utiliser le mot clé virtual et à donc privilégier le masquage.

    Qu'est ce qui ne va pas dans cet exemple ?

    J'ai cru comprendre que le masquage avait pour conséquence de masquer toutes les surcharges que j'aurais pu définir dans la classe A. Est ce la raison pour laquelle c'est déconseillé ?

  8. #8
    Modérateur

    Avatar de Bktero
    Homme Profil pro
    Développeur en systèmes embarqués
    Inscrit en
    Juin 2009
    Messages
    4 481
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 36
    Localisation : France, Loire Atlantique (Pays de la Loire)

    Informations professionnelles :
    Activité : Développeur en systèmes embarqués

    Informations forums :
    Inscription : Juin 2009
    Messages : 4 481
    Points : 13 678
    Points
    13 678
    Billets dans le blog
    1
    Par défaut
    J'avais effectivement oublié que ça masque tous les overloads... Voir https://stackoverflow.com/questions/...hiding-non-vir

    La raison principale pour moi est qu'il est trop facile d'oublier qu'il n'y a pas de virtualité. Et si par malheur une personne utilise les classes avec du polymorphisme, ça ne fera pas qu'elle attend et ça peut lui prendre un moment avant de comprendre l'origine de son bug.

    Certains analyseurs statiques de code alertent par défaut quand tu utilises une telle construction. C'est le cas de CLion par exemple :

    Nom : clion.png
Affichages : 160
Taille : 14,3 Ko

    De manière générale, il faut faire très attention quand on masque des choses, car il y a toujours un risque d'erreurs. On parle ici de fonctions dans des classes dérivées, mais il y a d'autres cas de masquage en C++. Si c'est possible, autant ne pas le faire, ça évite les erreurs. Si on souhaite vraiment le faire, il faut avoir de bonnes raisons et faire attention.

  9. #9
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 612
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 612
    Points : 30 611
    Points
    30 611
    Par défaut
    C'est en gros la raison pour laquelle C++11 est arrivé avec le mot clé override à placer en suffixe qui forcera le compilateur à s'assurer qu'il s'agit bel et bien d'une redéfinition et non d'un "simple" masquage:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
     
    class Base{
    public:
        void foo();
        virtual void bar();
    };
    class Derivee{
    public:
        void foo() override; // erreur: foo n'est pas une fonction virutelle
        void bar() override; // OK : la fonction est déclarée virtuelle et peut donc être redéfinie 
    };
    A noter qu'il est également possible d'interdire les redéfinitions suivante avec le mot clé final (également en suffixe)
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. Surcharge d'une méthode
    Par Pilloutou dans le forum C#
    Réponses: 8
    Dernier message: 10/10/2007, 15h53
  2. Réponses: 2
    Dernier message: 10/05/2007, 18h29
  3. [Débutant] Surcharge d'une méthode
    Par HaTnuX dans le forum Langage
    Réponses: 2
    Dernier message: 18/01/2007, 20h27
  4. Affichage sur plusieurs lignes d'une méthode toString
    Par Flophx dans le forum Interfaces Graphiques en Java
    Réponses: 9
    Dernier message: 24/05/2006, 17h30
  5. [MFC] Surcharge d'une méthode CListCtrl
    Par semenzato dans le forum MFC
    Réponses: 8
    Dernier message: 22/12/2005, 23h05

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo