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

 C++ Discussion :

Cas de conscience : Le retour d'objet est-il l'ennemi de l'optimisation ?


Sujet :

C++

  1. #1
    Candidat au Club
    Homme Profil pro
    Philosophe
    Inscrit en
    Mars 2014
    Messages
    3
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Philosophe

    Informations forums :
    Inscription : Mars 2014
    Messages : 3
    Points : 3
    Points
    3
    Par défaut Cas de conscience : Le retour d'objet est-il l'ennemi de l'optimisation ?
    Bonjour.

    Il y a une chose qui me perturbe fortement au sujet des fonctions qui retournent un objet.

    Le mieux c'est de prendre un exemple.
    Voici une classe :

    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
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    %:include <iostream>
     
    class ExampleString {
     
        public:
     
            ExampleString();
            ExampleString(const char* paramZeroString);
            ExampleString(const ExampleString &paramExampleString);
     
            void display();
     
            ~ExampleString();
     
        private:
     
            char* attrBuffer;
     
    };
     
     
     
    ExampleString::ExampleString() {attrBuffer = nullptr;}
     
     
     
    ExampleString::ExampleString(const char* paramZeroString) {
     
        unsigned int localSize = 0;
     
        if (paramZeroString != nullptr) {
     
            while (paramZeroString[localSize] != 0) localSize++;
     
            if (localSize > 0) {
     
                attrBuffer = new char[localSize + 1];
                for (unsigned int index = 0 ; index < localSize ; index++) attrBuffer[index] = paramZeroString[index];
                attrBuffer[localSize] = 0;
     
            }
            else attrBuffer = nullptr;
     
        }
        else attrBuffer = nullptr;
     
    }
     
     
     
    ExampleString::ExampleString(const ExampleString &paramExampleString) {
     
        ExampleString(paramExampleString.attrBuffer);
     
    }
     
     
     
    void ExampleString::display() {
     
        if (attrBuffer != nullptr) std::cout << '[' << reinterpret_cast<void*>(attrBuffer) << "] = " << attrBuffer << std::endl;
        else std::cout << "[nullptr]" << std::endl;
     
    }
     
     
     
    ExampleString::~ExampleString() {
     
        if (attrBuffer != nullptr) {
            delete[] attrBuffer;
            attrBuffer = nullptr;
        }
     
    }


    Bon, je viens de saisir le code à la volée sans le vérifier, j'ai peut-être oublié des choses mais l'important est en gros de considérer que : Nous avons une classe "ExampleString" qui gère un pointeur de données en interne, dont la mémoire est allouée dans le constructeur puis libérée dans le destructeur.

    Maintenant observons le code suivant :

    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
    %:include "class/ExampleString"
     
    ExampleString getBack() {
     
        ExampleString localExampleString = (char*)"BACK";
        return localExampleString;
     
    }
     
    int main() {
     
        ExampleString mainExampleString = getBack();
     
        mainExampleString.display();
     
        return 0;
     
    }


    Ce code implémente une fonction qui est sensée retourner un objet de type "ExampleString".
    Et là rassurez-moi, dites-moi que le langage C++ est bien fait, qu'il ne fait pas appel au constructeur de copie suivi du destructeur de l'objet, mais qu'il étend seulement la portée de la variable retournée à la fonction appelante...



    Bon j'avoue feindre l'ignorance, après vérification (sur gcc) il semblerait que malheureusement, lorsque ma fonction retourne l'objet "ExampleString" elle provoque un renouvellement dont j'aurais bien voulu me passer. Alors ma question est la suivante :
    Comment puis-je faire en sorte d'éviter ce genre d'écriture de mémoire ridiculement lourd et inutile ?



    Sachant que les deux solutions suivantes ont déjà été envisagées et semblent finalement hautement indésirables :

    1. Retourner un pointer au lieu d'un objet, mais cela demande de le détruire après chaque appel de la fonction, enfin ça donne une catastrophe conceptuelle quoi ;
    2. Créer des constructeurs et destructeurs intelligents à coup de mutables qui permettent à plusieurs instances de partager le même pointeur tant que les données n'ont pas besoin d'être modifiées. Ça résoud le problème mais ça alourdit énormément les méthodes de modification.



    Une idée ?



    Edit : Correction des erreurs dans le code présenté.

  2. #2
    Membre régulier
    Profil pro
    Inscrit en
    Janvier 2014
    Messages
    142
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Janvier 2014
    Messages : 142
    Points : 109
    Points
    109
    Par défaut
    Je me demande (je débute aussi...) si ta question n'a pas un rapport avec une question que j'ai posée ici.
    Si c'est bien cela chez moi la NRVO est active par défaut...

  3. #3
    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 577
    Points
    218 577
    Billets dans le blog
    120
    Par défaut
    Bonjour,

    Ici, je ne pense pas que le NRVO entre en jeu.
    Par contre, des réponses existent. Notamment, le C++11, a un mécanisme d'extension de portée, comme vous l'appelez. En effet, en C++11 (veuillez indiquer à G++ d'utiliser le C++11 et vous devriez avoir une amélioration), le mécanisme de transfert de propriétariat (std::move et consort), va entrer en jeu. Par contre, il vous faudra potentiellement implémenté un constructeur de déplacement.
    Sinon, si vous voulez le faire à la main, il y a les std::unique_ptr. Ou alors, une classe genre, Bank (qui stockera toutes les créations de ExampleString) mais en théorie, en C++11, cela ne devrait même plus exister, ce genre de classe.
    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.

  4. #4
    Membre expérimenté Avatar de Trademark
    Profil pro
    Inscrit en
    Février 2009
    Messages
    762
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Février 2009
    Messages : 762
    Points : 1 396
    Points
    1 396
    Par défaut
    Oui le constructeur par copie peut-être appelé quand tu fais un retour de ce type, ce qui est logique vu le fonctionnement d'une stack (comment voudrais-tu "étendre" la portée ?), il se peut que le compilateur fasse des optimisations par contre mais c'est pas dépendant du langage en lui-même. Néanmoins il existe au moins 3 solutions plus ou moins propre (dont 2 que tu n'as pas citées) :

    1. Passer ExampleString par référence et la fonction le modifie au lieu de le retourner.
    2. Passer par des pointeurs, grâce au unique_ptr et shared_ptr de C++11 ça ne provoque plus de catastrophe...
    3. Retourner l'objet grâce à la move semantics de C++11.


    Pour ce dernier point qui va probablement le plus t'intéresser, voici comment t'y prendre :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class ExampleString{
     
    // Déclaration du move constructeur
    ExampleString(ExampleString&& x)
    : attrBuffer(x.attrBuffer) {
      x.attrBuffer = nullptr;
    }
     
    ExampleString getBack() {
     
        ExampleString localExampleString("BACK"); // On ne cast pas en char* une chaine statique...
        return std::move(localExampleString);
    }
    Et voilà, il n'y aura plus de copie. Par contre évidemment, tu ne peux "bouger" que les données qui sont allouée sur le tas, ce qui ne pose quasiment jamais de problème vu que ce qui est gros et lourd et sur le tas.

    Quelques liens :

    http://en.cppreference.com/w/cpp/lan...ve_constructor
    http://en.cppreference.com/w/cpp/utility/move

  5. #5
    Rédacteur/Modérateur
    Avatar de JolyLoic
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2004
    Messages
    5 463
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 49
    Localisation : France, Yvelines (Île de France)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Août 2004
    Messages : 5 463
    Points : 16 213
    Points
    16 213
    Par défaut
    Citation Envoyé par Trademark Voir le message
    Oui le constructeur par copie peut-être appelé quand tu fais un retour de ce type, ce qui est logique vu le fonctionnement d'une stack (comment voudrais-tu "étendre" la portée ?), il se peut que le compilateur fasse des optimisations par contre mais c'est pas dépendant du langage en lui-même.
    Sachant que ce genre d'optimisation (RVO, NRVO) est vraiment standard, pour un code comme celui montré en exemple, je serais très surpris de trouver un compilateur mainstream qui ne la fait pas.
    Citation Envoyé par Trademark Voir le message
    Néanmoins il existe au moins 3 solutions plus ou moins propre (dont 2 que tu n'as pas citées) :

    - Passer ExampleString par référence et la fonction le modifie au lieu de le retourner.
    Ce genre de passage n'a d'intérêt qui si on compte utiliser le même ExampleString à plusieurs reprises. À part dans ce cas précis, il est moins clair, oblige à déclarer une variable (pas de chaînage d'appel), sans pouvoir lui donner une bonne valeur initiale qui plus est. Je le considère donc plutôt mauvais.
    Citation Envoyé par Trademark Voir le message
    - Passer par des pointeurs, grâce au unique_ptr et shared_ptr de C++11 ça ne provoque plus de catastrophe...
    Pareil, en pire (tu vas obliger une allocation dynamique de ta variable, ce qui n'est pas génial en terme de performances).
    Citation Envoyé par Trademark Voir le message
    - Retourner l'objet grâce à la move semantics de C++11.


    Pour ce dernier point qui va probablement le plus t'intéresser, voici comment t'y prendre :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class ExampleString{
     
    // Déclaration du move constructeur
    ExampleString(ExampleString&& x)
    : attrBuffer(x.attrBuffer) {
      x.attrBuffer = nullptr;
    }
     
    ExampleString getBack() {
     
        ExampleString localExampleString("BACK"); // On ne cast pas en char* une chaine statique...
        return std::move(localExampleString);
    }
    Le std::move final ne sert à rien : il a pour but d'indiquer que ce qu'on passe en argument peut être considéré comme une donnée temporaire, et donc être un bon candidat pour être déplacé et non plus copié (pour peu que l'objet soit déplaçable). Mais quand on est dans un return, toutes les variables locales d'une fonction sont déjà considérées comme des temporaires.

    Donc, et résumé :
    - En pratique, les compilateurs vont supprimer la copie dans des cas simples comme ici (tu peux t'en assurer en ajoutant des fonctions de trace dans tes opérations de copies/déplacement)
    - Et même dans les autres cas, si ta classe est déplaçable, il va la déplacer plutôt que de la copier

    Je parle un peu de move semantic dans ma présentation tech days 2014 (voir le lien dans ma signature).
    Ma session aux Microsoft TechDays 2013 : Développer en natif avec C++11.
    Celle des Microsoft TechDays 2014 : Bonnes pratiques pour apprivoiser le C++11 avec Visual C++
    Et celle des Microsoft TechDays 2015 : Visual C++ 2015 : voyage à la découverte d'un nouveau monde
    Je donne des formations au C++ en entreprise, n'hésitez pas à me contacter.

  6. #6
    Membre éprouvé
    Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Mars 2009
    Messages
    552
    Détails du profil
    Informations personnelles :
    Localisation : France

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

    Informations forums :
    Inscription : Mars 2009
    Messages : 552
    Points : 1 060
    Points
    1 060
    Par défaut
    Citation Envoyé par .-Îk_ Voir le message
    Bonjour.
    Bon j'avoue feindre l'ignorance, après vérification (sur gcc) il semblerait que malheureusement, lorsque ma fonction retourne l'objet "ExampleString" elle provoque un renouvellement dont j'aurais bien voulu me passer. Alors ma question est la suivante :
    Comment puis-je faire en sorte d'éviter ce genre d'écriture de mémoire ridiculement lourd et inutile ?
    Activer les optimisations du compilateur? gcc -O3 par exemple?

  7. #7
    En attente de confirmation mail

    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Août 2004
    Messages
    1 391
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 33
    Localisation : France, Doubs (Franche Comté)

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

    Informations forums :
    Inscription : Août 2004
    Messages : 1 391
    Points : 3 311
    Points
    3 311
    Par défaut
    Le cas que tu présentes est un cas où l'élision (2 élisions) est possible (le compilateur n'est pas obligé de le faire, mais si il ne le fait pas dans un tel cas, mets le à la poubelle, après avoir vérifié que c'est pas de la faute de tes options de compilation).

    @Trademark: Ton std::move "ne sert à rien", même en supposant que l'élision ne se fait pas. De manière général, la construction return std::move(); est inutile. La norme fait que dans le cas d'un return x; x est considéré comme une rvalue (il y a quelques conditions, mais elles sont généralement remplies). Ce qui rend totalement inutile l'ajout de std::move.

    PS: Exemple tiré de la norme :
    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
     
    struct Thing {
      Thing(){}
      ~Thing(){}
      Thing(Thing&&){}
      Thing(Thing&){}
    };
     
    Thing f(bool b) 
    {
      Thing t;
      if (b)
        throw t; // OK: Thing(Thing&&) used (or elided) to throw t
      return t; // OK: Thing(Thing&&) used (or elided) to return t
    }
     
    Thing t2 = f(false); // OK: Thing(Thing&&) used (or elided) to construct t2
    Ici le compilateur est autorisé à ne faire aucunes copies. L'effet est donc de construire directement l'objet t2 comme il faut. Cette exemple subit les mêmes règles que le tiens.

  8. #8
    Candidat au Club
    Homme Profil pro
    Philosophe
    Inscrit en
    Mars 2014
    Messages
    3
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Philosophe

    Informations forums :
    Inscription : Mars 2014
    Messages : 3
    Points : 3
    Points
    3
    Par défaut
    Merci à tous pour vos réponses, ça déblaie déjà pas mal la question !



    Citation Envoyé par JolyLoic Voir le message
    Sachant que ce genre d'optimisation (RVO, NRVO) est vraiment standard, pour un code comme celui montré en exemple, je serais très surpris de trouver un compilateur mainstream qui ne la fait pas.
    Citation Envoyé par bretus Voir le message
    Activer les optimisations du compilateur? gcc -O3 par exemple?
    Alors effectivement, j'ai présupposé à tort dans mon premier post que la fonction effectuerait une copie de l'objet retourné. Je viens de tester le code et finalement ce n'est pas le cas, la porté de la variable retournée est juste étendue.

    Ceci dit, si je suis venu poster ici avec de telles certitudes, c'est parce qu'il m'est arrivé d'observer une fonction "retournante" qui utilisait le constructeur de copie. Donc j'imagine que ça dépend de son implémentation ?


    Citation Envoyé par Trademark Voir le message
    Pour ce dernier point qui va probablement le plus t'intéresser, voici comment t'y prendre :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class ExampleString{
     
    // Déclaration du move constructeur
    ExampleString(ExampleString&& x)
    : attrBuffer(x.attrBuffer) {
      x.attrBuffer = nullptr;
    }
     
    ExampleString getBack() {
     
        ExampleString localExampleString("BACK"); // On ne cast pas en char* une chaine statique...
        return std::move(localExampleString);
    }
    Oui, le mouvement est très exactement la notion qui me manquait, merci beaucoup !!!

    Apparemment ce "std::move(localExampleString)" revient au même que "static_cast<ExampleString&&>(localExampleString)".
    Vu qu'on caste la variable en cet espèce de "machin à double esperluettes", on force plus ou moins l'appel du constructeur de déplacement ?

    En revanche le compilateur semble privilégier par défaut le déplacement à la copie, du coup s'il n'arrive pas à étendre la porté de la variable retournée, il va chercher le constructeur de déplacement, et si celui-ci n'existe pas il appelle le constructeur de copie ?
    Et par conséquent ça rend le cast contre-productif, car tant qu'on a défini le constructeur de mouvement tout va bien, non ? (C'est d'ailleurs peut-être ce que Flob90 tentait de dire ?)



    Citation Envoyé par BaygonV Voir le message
    Je me demande (je débute aussi...) si ta question n'a pas un rapport avec une question que j'ai posée ici.
    Si c'est bien cela chez moi la NRVO est active par défaut...
    Du coup ça me donne peut-être une idée qui pourrait t'intéresser :

    Si j'ai bien compris, il te suffit de faire un "return static_cast<type&>(var)", où "var" est le nom de la variable et "type" son type, pour forcer le constructeur de copie à fonctionner ! ^^

  9. #9
    Membre régulier
    Profil pro
    Inscrit en
    Janvier 2014
    Messages
    142
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Janvier 2014
    Messages : 142
    Points : 109
    Points
    109
    Par défaut
    Citation Envoyé par .-Îk_ Voir le message
    Si j'ai bien compris, il te suffit de faire un "return static_cast<type&>(var)", où "var" est le nom de la variable et "type" son type, pour forcer le constructeur de copie à fonctionner ! ^^
    Merci mais en fait je ne cherchais qu'à comprendre comment tout ça fonctionnait, je ne voulais pas forcer quoi que ce soit

  10. #10
    Expert éminent sénior
    Avatar de Médinoc
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Septembre 2005
    Messages
    27 369
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 40
    Localisation : France

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Septembre 2005
    Messages : 27 369
    Points : 41 518
    Points
    41 518
    Par défaut
    @.-Îk_: Je ne suis pas sûr que comprendre la (N)RVO en tant que "étendre la portée" soit très productif.
    Personnellement, le la comprends comme "construire l'objet sur place". Ne pas oublier que le retour d'objet se fait en vérité "par adresse" sur la plupart des compilos:
    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
    //On tape: 
    UneClasse UneFonction(int);
     
    int main(void)
    {
    	UneClasse obj = UneFonction(42);
    	return 0;
    }
     
    //On obtienti:
    UneFonction(UneClasse*, int);
     
    int main(void)
    {
    	__unconstructed UneClasse obj; //pseudo-syntaxe pour non-construction
    	UneFonction(&obj, 42); //La fonction construit l'objet
    	return 0;
    }
    Quant à la NRVO, elle consiste à construire directement la variable qu'on prévoit de retourner à l'emplacement de retour:
    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
    //On tape:
    UneClasse UneFonction(int a)
    {
    	UneClasse obj(a);
    	obj.FaireUnTruc();
    	return obj;
    }
     
    //On obtient:
    void UneFonction(UneClasse* pRet, int a)
    {
    	//NRVO! On considère que obj=*pRet
    	pRet->UneClasse(a); //pseudo-syntaxe pour appel de constructeur†
    	pRet->FaireUnTruc();
    }
    Ainsi, on voit facilement le cas non-optimisable par (N)RVO: C'est le cas où on ne sait pas à l'avance quelle variable va être retournée.
    Exemple:
    Code C++ : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    UneClasse UneFonction(a, b)
    {
    	UneClasse objA(a);
    	UneClasse objB(b);
    	objA.FaireUnTruc();
    	objB.FaireUnTruc();
    	if(rand() > RAND_MAX/2)
    		return objA;
    	else
    		return objB;
    }
    Ici, il faudra forcément une copie ou (si la classe UneClasse le supporte) un déplacement.

    †Oui, je sais qu'il existe une vraie syntaxe (placement new) mais je trouve qu'elle apporterait ici plus de confusion qu'autre chose.
    SVP, pas de questions techniques par MP. Surtout si je ne vous ai jamais parlé avant.

    "Aw, come on, who would be so stupid as to insert a cast to make an error go away without actually fixing the error?"
    Apparently everyone.
    -- Raymond Chen.
    Traduction obligatoire: "Oh, voyons, qui serait assez stupide pour mettre un cast pour faire disparaitre un message d'erreur sans vraiment corriger l'erreur?" - Apparemment, tout le monde. -- Raymond Chen.

Discussions similaires

  1. Javascript et retour d'objet
    Par roudoudouduo dans le forum Général JavaScript
    Réponses: 5
    Dernier message: 27/04/2006, 15h47
  2. retour d'objet par référence...
    Par sas dans le forum C++
    Réponses: 15
    Dernier message: 28/05/2005, 17h54
  3. Réponses: 6
    Dernier message: 06/12/2004, 22h18

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