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

Langage C++ Discussion :

Coût pour appeler des méthodes de sa classe mère ?


Sujet :

Langage C++

  1. #1
    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 679
    Points
    13 679
    Billets dans le blog
    1
    Par défaut Coût pour appeler des méthodes de sa classe mère ?
    Bonjour,

    En essayant d'optimiser une classe et notamment une de ces méthodes, j'ai créé une classe dérivée dans laquelle j'override la méthode en question. Je me suis rendu compte, mesures à l'appui, que si ma fonction redéfinie rappelle directement la méthode parente, elle est plus rapide que si je copie-colle le corps de la méthode de base dans celui de la dérivée. En gros, ça donne quelque chose comme ça :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    class Base {
    public:
    	virtual void flush() {
    		for(...)
    			doStuff();
    	}
     
    protected:
    	void doStuff() {
    		// stuff
    	}
    };
     
    class Derived : public Base {
    public:
            // Plus rapide
    	void flush() override {
    		Base::flush();
    	}
     
            // Plus lente
    	void flush() override  {
    		for(...)
    			doStuff();
    	}
    };
    La différence n'est pas négligeable... Pour mon test, la version rapide met 126 ms et la version lente 143 ms

    Des idées de pourquoi ?

    Merci d'avance !

  2. #2
    Membre expert
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2011
    Messages
    739
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Hérault (Languedoc Roussillon)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juin 2011
    Messages : 739
    Points : 3 627
    Points
    3 627
    Par défaut
    J'aurais une explication totalement farfelue où le compilateur déduis qu'il n'existe qu'une implémentation de flush (celle de la classe fille étant un alias) et qu'il en profite donc pour dé-virtualiser la fonction.

    Mais il faudrait avoir un test complet et regarder l'assembleur (https://gcc.godbolt.org/ est pas mal pour ça) ou compiler avec des options pour avoir un assembleur plus lisible (-S -fverbose-asm pour gcc, -S -flto pour clang).

  3. #3
    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 679
    Points
    13 679
    Billets dans le blog
    1
    Par défaut
    Hier soir, je me suis dit : "bon sang mais c'est bien sûr ! Le fichier base.cpp est compilé en O3 alors que le fichier derived.cpp utilise le niveau par défaut qui est Og ! C'est ça !" Mais non : je passe de 143 à 139 ms...

    Pour tester ton hypothèse de virtualité, j'ai enlevé le mot-clé virtual de l'interface qui est encore au-dessus de la classe Base. Ca change....dans le mauvaise sens ! De 126/139, je passe à 130/144 ms

    Malheureusement il y a beaucoup de codes et de fichiers pour utiliser le décompilateur en ligne. Un collègue avait tenté de l'installer en local, je vais voir s'il a réussi. Après il me reste toujours objdump mais ça pique un peu pour analyser tout ça.

  4. #4
    Expert éminent sénior
    Avatar de Kannagi
    Homme Profil pro
    cyber-paléontologue
    Inscrit en
    Mai 2010
    Messages
    3 214
    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 214
    Points : 10 140
    Points
    10 140
    Par défaut
    Et tenter de faire un -S ?
    Après il faut savoir lire un peu l'asm ah ah

  5. #5
    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 679
    Points
    13 679
    Billets dans le blog
    1
    Par défaut
    Ben c'est ce qu'il va falloir faire. Merci de me rappeler l'option -S ça pourrait être un peu mieux !

    J'ai testé de vraiment dupliquer Base dans Derived et j'ai changé les protected en private. Ainsi Derived override certes, mais rappelle bien ses propres fonctions. Les temps sont alors les mêmes pour les 2 (126 ms). C'est visiblement bien une histoire d'appeler la classe parente depuis la dérivée...

  6. #6
    Responsable 2D/3D/Jeux


    Avatar de LittleWhite
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Mai 2008
    Messages
    26 858
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Mai 2008
    Messages : 26 858
    Points : 218 575
    Points
    218 575
    Billets dans le blog
    120
    Par défaut
    Bonjour,

    Les 126 ms, c'est sur 1 appel ? ou sur plusieurs (combien ?) ?
    Vous souhaitez participer à la rubrique 2D/3D/Jeux ? Contactez-moi

    Ma page sur DVP
    Mon Portfolio

    Qui connaît l'erreur, connaît la solution.

  7. #7
    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 679
    Points
    13 679
    Billets dans le blog
    1
    Par défaut
    Pour donner un peu plus de contexte, flush() permet de pousser le frame buffer vers l'écran OLED via liaison SPI sur mon système embarqué. 126 ms correspondent à 3 appels à flush(). doStuff() c'est (grosso modo) l'envoi d'un pixel. J'envoie 128*128 pixels. Il a des pre- et des post-actions autour de la boucle dont les coûts sont négligeables.

  8. #8
    Expert éminent sénior
    Avatar de Kannagi
    Homme Profil pro
    cyber-paléontologue
    Inscrit en
    Mai 2010
    Messages
    3 214
    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 214
    Points : 10 140
    Points
    10 140
    Par défaut
    Tu es sur que c'est pas les transferts qui sont longs ? (bref plus un souci bas niveau).
    Sinon je pense qu'on pourra t'aider pour le code asm si tu le poste :p

  9. #9
    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 679
    Points
    13 679
    Billets dans le blog
    1
    Par défaut
    Les deux classes font la même chose et le résultat produit est OK dans les 2 cas. J'opte donc une différente de l'assembleur généré pour envoyer les bytes sur le bus SPI.

  10. #10
    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 612
    Points
    30 612
    Par défaut
    Salut,

    Personnellement, je mettrais bien cela sur le fonctionnement de la "table des fonctions virtuelles", les optimisations que le compilateur peut faire à partir de là, et de la "promiscuité" des différentes données, qui peuvent (ou non) améliorer l'utilisation des caches les plus rapides.

    Car, si j'ai bien compris (ce qui reste malgré tout encore à confirmer ), quand on déclare des fonctions virtuelles dans une classe de base, le compilateur va créer une table proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
     
    fonction | adresse |
             | de base |
    ---------+---------|
    foo      | 0x ...  |
    bar      | 0x ...  |
    dtor     | 0x ...  |
       ...   |   ...   |
    Et, quand ont va redéfinir une fonction virtuelle, la table des fonctions virtuelle ressemblera à quelque chose comme
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    fonction | adresse |   |adresse|
             | de base |   |finale |
    ---------+---------|
    foo      | 0x ...  | ->|0x ... | 
    bar      | 0x ...  |
    dtor     | 0x ...  |
       ...   |   ...   |
    où le lien qui permet de passer de l'adresse de base à l'adresse finale s'apparente à l'implémentation d'une file ou d'une liste.

    et -- à condition toujours que j'ai bien compris le principe -- que, lorsqu'un classe hérite d'une autre, le compilateur se contente de recopier la table des fonctions virtuelles de la classe de base (avec les liens vers les adresses des fonctions redéfinies "avant") dans la classe dérivée. Si bien que, avec une hiérarchie de classes qui pourrait ressembler à quelque chose comme
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Base{
    public: 
        virtual void foo();
    };
    class D1 : public Base{
    public:
        void foo() override;
    };
    class D2 : public D1{
    public:
        void foo() /*final*/ override;
    };
    la table des fonctions virtuelles de ressemblerait à quelque chose comme
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
     
    fonction | adresse |    |adresse |    |adresse |
             |  Base   |    |   D1   |    |   D2   |
    ---------+---------|
    foo      | 0x ...  | -> |0x ...  | -> | 0x ... |
    Alors, bien sur, ce qui suit va partir du principe que j'ai effectivement bien compris l'idée (ce qui reste à démontrer), mais, si c'est le cas:

    L'accès à l'adresse à laquelle se trouve l'implémentation de la fonction "la plus redéfinie" se fait avec une complexité en O(N): pour atteindre l'adresse mémoire à laquelle on trouvera l'implémentation de foo propre à D2, nous aurons du parcourir les adresses mémoire auxquelles se trouvent l'implémentation de foo qui est propre à base et à D2.

    Et, pas de bol, les données représentée sous une telle forme peuvent régulièrement s'avérer moins "cache friendly".

    A l'inverse, lorsque tu écris un code proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    class D2 : public D1{
    public:
       void foo() /* final */ override{
          Base::foo();
       }
    }
    Le compilateur pourra aller chercher directement l'adresse mémoire à laquelle se trouve l'implémentation de foo pour la classe de base, vu que c'est la première adresse à laquelle il va accéder.

    Et, du coup, il pourrait sembler "normal" qu'il puisse faire des optimisations à ce sujet

    Bien sur, tout ce développement ne vaudra pas tripettes s'il devait s'avérer que je n'ai pas tout compris du fonctionnement des tables de fonctions virtuelles
    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

  11. #11
    Membre expert
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2011
    Messages
    739
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Hérault (Languedoc Roussillon)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juin 2011
    Messages : 739
    Points : 3 627
    Points
    3 627
    Par défaut
    Il n'y a pas autant d’indirection qu'il y a de niveau d'héritage. Avec un tel procédé, il faudrait ajouter une information pour savoir si le pointeur correspond à une fonction finale ou non et le coup d'un appel serait vraiment très coûteux.

    Chaque type possède une table des fonctions virtuelles (vtable) autonome est complète (qui contient aussi les fonctions qui ne sont pas redéfinies). Au type de base possédant des membres virtuel s'ajoute un membre caché qui n'est ni plus ni moins qu'un pointeur vers cette table. Lorsque qu'on utilise une fonction virtuelle, il y a une indirection vers la vtable à un indice donné représentant le pointeur de fonction à utiliser. Lorsqu'une classe fille ajoute de nouvelle fonction virtuelle, le pointeur caché pointe juste sur une table plus grande, la position des fonctions de base dans la vtable ne bouge pas et le coup d'un appel est d'une complexité constante.

    Comme les différences doivent être asse minimum, le plus simple est de faire une comparaison directement sur l'assembleur (-S -fverbose-asm). Mais le test peut aussi être en cause. C'est très facile de faire un test en apparence représentatif, mais sur lequel le compilateur est libre de faire beaucoup d'optimisation qu'il n'aurait pas du. Par exemple, il ne faut pas mettre les données du test (si écrite en dures) dans la même unité de compilation que le test. Ni utilisé des options tel que -ftlo.

Discussions similaires

  1. Réponses: 1
    Dernier message: 01/11/2012, 23h44
  2. Choix d'une classe pour appel de méthodes statiques
    Par Antwan76ers dans le forum Débuter avec Java
    Réponses: 11
    Dernier message: 05/07/2012, 10h55
  3. Problème pour appeler une méthode d'une autre classe
    Par tse_tilky_moje_imja dans le forum Général Python
    Réponses: 7
    Dernier message: 03/03/2006, 13h33
  4. [POO]Appelé une méthode d'une classe mère
    Par LE NEINDRE dans le forum Langage
    Réponses: 2
    Dernier message: 14/12/2005, 14h44

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