C++17 garantit le court-circuit de copie (suite de la précédente dépêche)

Posté par  . Édité par Oliver, Davy Defaud, David Marec, Snark, Benoît Sibaud et lmg HS. Modéré par bubar🦥. Licence CC By‑SA.
Étiquettes :
28
13
déc.
2016
C et C++

Le calendrier de l’Avent du C++ continue son bonhome de chemin. Chaque jour, ou presque, une nouvelle surprise est offerte aux lecteurs de LinuxFr.org. La dépêche sur l’élision de la copie nous a mis l’eau à la bouche :

Comment résoudre le dilemme entre cette optimisation et le fait de ne pas pouvoir en bénéficier dans un code portable ?

Alors, entrons dans les entrailles de la spécification technique P0135 guidés par cette dépêche pédagogique, et découvrons comment le C++ s’améliore de version en version.

Une fille nerd s’électrocute en touchant la nouvelle tour C++17 de sa voisine avec garantie de court-circuit de copie

Sommaire

La problématique

Nous avons vu dans la dépêche C++ se court‐circuite le constructeur de copie, les problèmes de l’élision du constructeur de copie.

En effet, cette optimisation est optionnelle : selon le compilateur, sa version, ses options de compilation, ou encore selon le code source, l’élision sera appliquée ou pas.

Donc, dans un code portable, de façon générale, nous ne pouvons pas nous passer des constructeurs de copie et de déplacement, car l’élision pourrait ne pas avoir lieu. Cela interdit aux types indéplaçables d’avoir des fonctions qui retournent par valeur, telles que les fabriques.

int n; // compte le nombre d’appels du constructeur de copie

struct A
{
  A(int) {}
  A(const A&) { ++n; }
};

int main()
{
  A a = A( A( A( A(42) ) ) );
  return n; // valeur non déterminée avant C++17
}           // toujours 0 avec C++17

L’astuce de la TS P0135

La spécification technique P0135 ne rend pas obligatoire l’élision. Pour la garantir, cette TS redéfinit la taxonomie des expressions dans le but d’éviter toute création inutile d’objet temporaire. Le cas des Named Return Value Optimizations n’est pas concerné par cette spécification technique, faute de pouvoir apporter une garantie simple.

Une taxonomie des expressions ?

La taxonomie est la science du classement, pour identifier et décrire le vivant. Un taxon est un groupe d’organismes classés ensemble parce qu’ils ont des caractéristiques communes. Comme des poupées russes, un taxon peut contenir un sous‐ensemble plus petit de taxons. Voir aussi le documentaire Espèces d’espèces.

Le C++ reprend ce principe pour classer ses expressions. La classification historique (C++98) définit deux taxons : lvalue et rvalue.

À l’origine, ces termes ont été choisis car, dans les cas simples, le lvalue se trouve à gauche (left value) et le rvalue (right value) se trouve à droite. Mais cette disposition n’est pas toujours vraie.

left_value = right_value;
\ \
`--se trouve `--se trouve
à gauche à droite

Chacun des taxons a des propriétés spécifiques. Par exemple, seul lvalue peut être la cible d’une affectation.

Taxonomie C++98

Note : La norme définit un objet comme une zone mémoire pouvant contenir des données.

Une taxonomie contre‐intuitive

La taxonomie du C++11 a inscrit la nouvelle catégorie xvalue, nécessaire pour la sémantique de déplacement (move semantics). Ce qui a rendu l’évaluation des expressions C++ plus confuse et difficile à s’approprier :

Taxon Caractères discriminants
lvalue Désigne une fonction ou un objet
xvalue Un objet proche de la fin de vie et certains types d’expressions impliquant des références rvalue
rvalue Une xvalue, un objet temporaire, un sous‐objet ou une valeur pas associé avec objet.
prvalue Une rvalue qui n’est pas une xvalue
glvalue Taxon supérieur d’une lvalue et xvalue

Ainsi, il y a des contradictions dans de nombreux cas. Par exemple, une expression qui crée un objet temporaire est une rvalue. Alors que selon les critères discriminant, elle aurait pu tout aussi bien être une lvalue.

Autre paradoxe, une expression qui construit un objet puis accède à un membre, telle que A().a est une xvalue. En effet, l’objet construit A() expire à la fin de l’évaluation de l’expression. Or, dans une situation où il est indéplaçable, c’est contradictoire avec une xvalue, ce qui sous‐entend être déplaçable. Donc, elle devrait être une prvalue. On y perd son latin !

Ci‐dessous, un schéma très simplifié, pour mieux comprendre ce classement :

Taxonomie C++11

Redéfinir pour garantir l’élision de copie

La spécification technique propose de faire la distinction entre les expressions prvalue et glvalue. Désormais, une glvalue définit la localisation d’un objet, une prvalue son initialisation :

Taxon Caractères discriminants
glvalue Identifie une fonction, un objet, un champ de bits
prvalue Initialise un objet, un champ de bits
xvalue une glvalue dont la ressource peut être réutilisée
lvalue Une glvalue qui n’est pas une xvalue
rvalue Une prvalue qui n’est pas une xvalue

Taxonomie C++17

struct X { int n; };
extern X x;
X{4};   // prvalue car initialise un objet temporaire X
x.n;    // glvalue car localise l'attribut n de x
X{4}.n; // glvalue car localise l'attribut n de X{4}

using T = X[2]; 
T{{5}, {6}};    // prvalue car initialise un tableau de X

Si une prvalue est utilisée pour initialiser un objet de même type, il le sera directement. Par conséquent, l’initialisation de la valeur de retour d’une fonction avec un temporaire entraîne l’initialisation directe de la valeur, sans copie, ni déplacement. Cela signifie que le constructeur de copie ou de déplacement de l’objet n’a plus besoin d’être défini.

struct NonDeplacable {
  NonDeplacable(int);
  NonDeplacable(NonDeplacable &)  = delete;
  NonDeplacable(NonDeplacable &&) = delete;
  std::array<int, 1024> arr;
};

NonDeplacable make() {
  return NonDeplacable(42); // Construit directement l'objet renvoyé
}

auto nm = make(); // Construit directement l'objet renvoyé dans 'nm'

NonDeplacable x = {5}; // Ok avant C++17
NonDeplacable x = 5;  // Équivaut à NonDeplacable x = NonDeplacable(5)
                      // Erreur avant C++17 parce qu'il crée un objet non déplaçable 
                      // Maintenant, plus d'erreur

Ce changement est sans conséquence pour l’optimisation Named Return Value Optimization (NRVO). Comme mentionné précédemment, la modification implique seulement les prvalues. Avec la NRVO, la valeur retournée est une glvalue.

A f()
{  
  A a;
  return a; // Retourne une glvalue
}           // Donc sans garantie d'élision

int main()
{
  A a = f();
}

Et les xvalues ?

La définition d’une xvalue gagne en clarté. Désormais, elle désigne une expression dont la ressource (les données) peut être réutilisée. Ce qui correspond bien à un objet déplaçable, dont ses données peuvent être réutilisées dans un autre objet.

Dernier changement, le compilateur est autorisé à convertir l’initialisation d’un objet temporaire (prvalue) vers une xvalue.

struct X { int n; }
int k = X().n; // X() est une prvalue convertie en xvalue.

Conclusion

Nous venons d’aborder un sujet complexe du C++. Nous pourrions trouver le C++ beaucoup trop compliqué. Mais cette dépêche a montré que le standard se simplifie. Un standard C++ plus compréhensif, de meilleures optimisations et un code plus intuitif. Que demander de plus ?

Réutilisation

Le texte de cette dépêche est protégé par le droit d’auteur la gauche d’auteur et réutilisable sous licence CC BY-SA 4.0. Les images utilisées sont aussi sous licence libre (cliquer sur l’image pour plus de détails).

Donc, n’hésitez pas à réutiliser ce contenu libre pour créer, par exemple, des supports de formation, des présentations (Meetups), des publications sur d’autres blogs, des articles pour des magazines, et aussi un article C++17 sur Wikipédia dès que Wikipédia passera de la licence CC BY-SA 3.0 à la CC BY-SA 4.0 (le contenu de cette dépêche utilise la version la CC BY-SA 4.0).

Les auteurs

Par respect de la licence, merci de créditer les auteurs :

Continuer à améliorer ce document

Malgré tout le soin apporté, il reste certainement des oublis, des ambiguïtés, des fôtes… Bien que cette dépêche restera figée sur le site LinuxFr.org, il est possible de continuer à l’enrichir sur le dépôt Git du Groupe des utilisateurs C++ francophone (C++FRUG). C’est donc sur ce dépôt que se trouvent les versions les plus à jour.   (ღ˘⌣˘ღ)

Alors que cet article restera figé sur le site LinuxFr.org, il continuera d’évoluer sur le dépôt Git. Merci de nous aider à [maintenir ce document à jour][md] avec vos questions, suggestions et corrections. L’idée est de partager ce contenu libre et de créer et enrichir des articles Wikipédia, quand la licence sera CC BY-SA 4.0.   ٩(•‿•)۶

Appel à contribution

Nous nous efforçons de vous fournir une dépêche de qualité chaque jour ouvré. Et, en plus, avec une illustration sympathique. Mais cela demande beaucoup de travail, et tenir les délais n’est pas toujours simple.

Merci de nous donner un coup de main, que ce soit sur la correction orthographique, le dessin, la lecture des spécifications techniques, la rédaction d’une nouvelle dépêche à intégrer au calendrier de l’Avent du C++. Tu peux aussi apporter ta contribution à la dépêche Faut‐il continuer à apprendre le C++ ?

Rejoins‐nous sur la page du groupe de travail C++ sur LinuxFr.org (un compte est nécessaire pour y accéder).

À suivre…

Aller plus loin

  • # C'est ma dépêche préférée.

    Posté par  . Évalué à 3.

    Merci de éclaircissement sur les lvalues et rvalues ainsi que les nouveautés.
    De mon point de vue, ces définitions devraient être expliquées et apprissent dès le début, on lès voit partout.
    Perso dans mes cours de C et de pseudo C++ (années 90), ces définitions n'étaient jamais données, par la suite lire des bouquins "d'expert" (en anglais) devenait vite compliqué à lire.

    • [^] # Re: C'est ma dépêche préférée.

      Posté par  . Évalué à 3.

      A mon avis, cela n'était pas enseignée parce que cela n'avait pas d'utilité pratique pour la majorité des devs.

      Avec le C++11, on a maintenant les rvalue references et les lvalue références, ce qui fait que connaître ces deux catégories de valeurs est intéressant. Mais les autres catégories, on s'y intéresse pas trop pour le moment.

    • [^] # Re: C'est ma dépêche préférée.

      Posté par  . Évalué à 6.

      Moui, si t'essayes d'enseigner ça dans un cours d'apprentissage de la programmation, tu vas perdre genre 90% des étudiants en quelques secondes.

      • [^] # Re: C'est ma dépêche préférée.

        Posté par  . Évalué à 1.

        Ca tombe bien, le C++ correspond a environ 10% des langages de programmation en termes d'utilisation. On est raccord :)

        Si tu parles des lvalues/rvalues, je ne crois pas que cela pose de problème d'expliquer ca a des débutants. En adaptant les explications bien sur. Il est possible de simplifier, en première approche, avec "lvalue = variable et rvalue = temporaire". Et c'est bon.
        Il est clair que si tu commences la seconde heure d'un cours C++ par expliquer toutes les catégories de valeurs, tu vas perdre beaucoup plus de 10% des étudiants.

      • [^] # Re: C'est ma dépêche préférée.

        Posté par  . Évalué à 2.

        Un cours d'apprentissage doit aussi te permettre de comprendre des bouquins, les termes rvalue et lvalue sont très souvent utilisés. Alors au minimum, je pense qu'un cour de C ou C++ doit en parler.

  • # Taxonomie

    Posté par  . Évalué à 4.

    C'est pratique d'avoir un synonyme dans un langage orienté objet, mais grosso-modo "taxonomie" c'est un synonyme de "classification".

    Ce qui fait que "taxon", essentiellement, c'est la même chose que "classe". Bon, certes ici on fait de la classification d'expression, et nos classes ne sont pas des types de C++ :)

    Certes, taxonomie est plus souvent employé de manière spécifique pour la classification du vivant, mais c'est aussi employable de manière plus générique.

    • [^] # Re: Taxonomie

      Posté par  . Évalué à 1.

      Le terme "taxonomie" vient en fait de la norme C++, les auteurs de la dépêche ne font que reprendre le terme.

      Expressions are categorized according to the taxonomy in Figure 1.

      Je préfère parler de "catégorie de valeurs", comme sur cppreference.com, plutot que de "taxons".

      • [^] # Re: Taxonomie

        Posté par  . Évalué à 3.

        Il y a pleins de mots pour parler grosso-modo de la même chose : classe, sorte, type, catégorie …

        On a le choix :)

  • # Value Semantics Rocks

    Posté par  . Évalué à 7.

    Ces optimisations, faites par tout compilateur qui se respecte, sont vraiment essentielles à connaitre. Leur meilleur connaissance permettrait à beaucoup de programmeurs de réaliser que pour des raisons de performance, il est important de renvoyer les objets par valeur.

    On voit malheureusement encore beaucoup de personnes écrire des signatures comme :

    void f(const T& input, U& output);

    lorsque U est un objet un peu gros comme un std::vector ou un std::array. Souvent l'argument donné est que cela permet d'éviter d'avoir à faire une copie. Mais comme l'explique très bien cet article, c'est inutile car le compilateur peut (en respectant une ou deux règles simples) faire un court-circuit de la copie. Et il n'y a pas besoin de move semantics pour que cela soit efficace, donc n'hesitez pas à renvoyer par valeur des std::array. En conclusion, utilisez plutôt la signature :

    U f(const T& input)

    Cette signature permet de plus aux compilateurs de nombreuses optimisations impossibles avec la première signature. Prenez par exemple cette fonction:

    void f(const std::array<double, 2>& x, std::array<double>& y) {
        y[0] = std::cos(x[0] + x[1]);
        y[1] = std::sin(x[0] + x[1]);
    }

    Elle ne peut pas être optimisée pour son usage courant. En effet, il faut savoir qu'on peut calculer très efficacement le cos et le sin d'un même angle (avec une seule instruction x86). Cela semble être le moment idéal pour utiliser cette instruction. Malheureusement, c'est impossible, car si on appelle f(x, x), on voit bien que la première ligne change x[0] et donc que les deux angles ne sont pas les mêmes. Si par contre on utilise la signature:

    std::array<double, 2> f(const std::array<double>& x)

    le compilateur peut faire l'optimisation et le code sera 2 fois plus rapide. Essayez d'écrire les deux fonctions et regardez l'assembleur produit (ou mieux mesurez le temps d'execution) et vous verrez qu'avec un bon compilateur (le compilo intel, ou une version récente de gcc), la seconde signature est beaucoup plus efficace. De nombreuses librairies, comme Boost::odeint limitent fortement leur performance car elles imposent la première signature.
    De manière générale, l'aliasing induit par ces "retour par référence" peut être catastrophique pour la performance. J'ai réussit une fois à faire une x3 en performance rien qu'en changeant une telle signature. Comme il n'y avait plus d'aliasing sur des std::array, le compilateur a pu tout reordonner les calculs et pu profiter à fond du parallélisme d'instructions.

    Bref, pour écrire des codes performants, renvoyez par valeur. Chandler Carruth, qui s'occupe de l'optimiseur de LLVM est un grand militant pour cela. Regardez ses conférences qui sont très instructives.

    Enfin, afin de donner un contre exemple, il est quand même parfois intéressant d'avoir un retour par référence. comme sur ce code:

    std::string line;
    while(file.get_line(line)) {
        // process line
    }

    Comme les fichiers ont souvent des lignes de moins de 80 (ou 120) caractères, il va y avoir une allocation dynamique de mémoire au début de la boucle, puis plus aucune ensuite, sauf si la ligne grossit, mais c'est rare. Ma politique est de toujours renvoyer par valeurs, sauf si vous avez un benchmark clair qui montre que on est dans ce type de cas.

    Au passage, je me permet de faire la pub pour un projet libre perso : des conteneurs efficaces pour le calcul haute performance. C'est ici : https://github.com/insideloop/InsideLoop

    • [^] # Re: Value Semantics Rocks

      Posté par  (site web personnel) . Évalué à 4.

      Mes deux centimes: si vraiment tu veux faire de la pub, fait un journal. Ça oblige à rédiger un petit descriptif du projet mais ca sera plus visible …

    • [^] # Re: Value Semantics Rocks

      Posté par  (site web personnel) . Évalué à 2.

      Au passage, je me permet de faire la pub pour un projet libre perso : des conteneurs efficaces pour le calcul haute performance. C'est ici : https://github.com/insideloop/InsideLoop

      Intéressant, la partie où tu mentionnes que stroustrup & co regrettent d'avoir utilisé des entiers non signés pour les conteneurs de la STL ! J'ai tendance à utiliser des unsigned partout du coup ça donne à réfléchir

      • [^] # Re: Value Semantics Rocks

        Posté par  . Évalué à 4. Dernière modification le 14 décembre 2016 à 23:32.

        J'ai beaucoup étudié les différents entiers et leur rapprt usage / performance, mon travail consistant à faire de la formation et du conseil en calcul HPC.

        Entre les signés et les non signés, pour moi le choix est clair : il faut utiliser les entiers signés, sauf si on veut absolument que l'entier se comporte modulo 2n, ou si malheureusement on y est contraint par la STL. Je sais que je peux m'attirer les foudres de nombreux programmeurs C++, mais Stroustrup, Sutter et Carruth sont avec moi, donc je me sens moins seul :-). Les entiers non signés devraient d'ailleurs être appelés entiers "modulo". Les entiers signés sont beaucoup moins dangereux que les non signés. Je donne un gag sur ma page, mais un autre est le suivant :

        for (std::size_t k = 0; k < point.size() - 1; ++k) {
            draw_line(point[i], point[i +1]);
        }

        Le jour où point ne contient aucun, élément, c'est la catastrophe.

        Au niveau performance, c'est 50 / 50 :
        - La division est un peu plus rapide pour les entiers non signés. Pour les autres opérations c'est exactement pareil car ce sont les mêmes opérations au niveau assembleur.
        - De nombreuses optimisations peuvent être faites sur les entiers signés comme : i < i + 1 est toujours vrai, a[i] et a[i + 1] sont cote à cote dans la mémoire, 10* i / 5 = 2 * i. Vous verrez que tout cela est faux pour les types non signés. Je vois souvent des compilateurs refuser certaines optimisations comme la vectorisation car les types non signés leur font peur. Une analyse fine montre souvent que cette vectorisation était quand même posssible, mais comme certaines autres ne le sont pas, la vue de ces entiers doit faire fuir les optimseurs :-)
        Donc, mon conseil est de toujours traiter avec des types signés et de faire un static_cast vers un unsigned (ça ne génère aucun code assembleur) si on doit faire de nombreuses divisions dans une partie critique d'un code avec des entiers signés dont on sait qu'ils sont positifs. Au passage, cela ne m'est jamais arrivé de le faire :-)

        Concernant la performance des entiers 32 bits ou 64 bits, pour indexer les boucles (je ne veux pas entendre parler d'itérateurs pour les tableaux, car les indices sont beaucoup plus pratique : on peut faire leur moyenne, ils ne sont pas invalidés quand on fait un resize, il n'existe pas d'iterateur efficace pour les tableaux multidimensionnels dès qu'on fait du padding, etc), il faut savoir que pour calculer p[i] lorsque p est un pointeur et i un entier, il faut une instruction d'assembleur pour faire passer i du 32 bit au 64 bit avant de l'ajouter à p. Donc les entiers 64-bit seraient en pratique un peu plus efficace que les entiers 32 bits pour indexer. Maintenant, les compilateurs font tellement de transformation sur une boucle (unrolling, vectorisation) que cette instruction n'a en pratique aucun effet notable. Bref, le match est 50-50 pour moi.
        Par contre lorsqu'un traite des tableaux d'indices (on a beaucoup d'entiers), la il faut prendre le type le plus petit possible (donc 32-bit) pour que la vectorisation soit le plus efficace possible.
        Sur d'autres algorithmes, cela dépend. Si par exemple, on cherche le i tel que v[i] est minimal dans un std::vector, il vaut mieux utiliser un entier 64-bit car les doubles sont sur 64-bits. Si on remplace le double par un float, il vaut mieux un indice codé sur 32-bit, comme le float. Cela permet au compilateur de gérer les lignes vectorielles plus efficacement.

        En gros, dans un monde sans STL, mon conseil pour la performance et les indices des boucles est le suivant : utilisez un entier signé. Quand à savoir si il faut le prendre en 32 bit ou 64-bit, faites ce que vous voulez, mais sachez qu'il est parfois nécessaire de choisir le bon pour la performance ultime.

        • [^] # Re: Value Semantics Rocks

          Posté par  (site web personnel) . Évalué à 1.

          Tu peux rajouter Eric Niebler (cf commentaires) à la liste des personnes notables qui préfèrent les signés.

          Pour ma part je suis toujours un peu dubitatif faute d'avoir pris le temps d'explorer toutes les possibilités et désireux d'avoir une garantie de positivité, mais je sens que nous sommes dans un débat du même acabit que NULL VS 0: le machin sémantiquement fort est un gros piège. En ce moment, je suis plus dans le trip de vouloir savoir si de bosser avec des types opaques du niveau domaine de définition va apporter quelque chose en termes de sécurité: https://openclassrooms.com/forum/sujet/types-opaques-domaines-de-definitions?page=1

          BTW, je me trompe ou ton exemple marcherait avec:

          for (std::size_t k = 1; k < point.size(); ++k) {
              draw_line(point[k-1], point[k]);
          }

          Je n'achète pas non plus l'exemple précis du "find_last index qui vérifie v[k] == 0"—sur le github. Probablement parce que ce n'est pas un problème que j'ai fréquemment à résoudre. J'ai rarement besoin de boucler à l'envers.

          • [^] # Re: Value Semantics Rocks

            Posté par  . Évalué à 1.

            Je n'achète pas non plus l'exemple précis du "find_last index qui vérifie v[k] == 0"—sur le github. Probablement parce que ce n'est pas un problème que j'ai fréquemment à résoudre. J'ai rarement besoin de boucler à l'envers.

            Je l'ai personnellement rencontré plusieurs fois. La dernière fois, c'était pour une détection d'un gros objet noir sur un fond blanc. Pour détecter le bord droit, on partait du côté droit et on avançait vers la gauche jusqu'à trouver du noir. Comme il y avait un rectangle de recherche à l'intérieur de l'image, on se retrouvait à gérer les a et les b comme dans le code présenté. Le code marchait très bien jusqu'au jour où une personne à mis la gauche du rectangle de recherche sur le bord gauche de l'image (a == 0) et qu'un objet n'était pas présent. Ce genre de bug n'avait a été découvert par un client. Et quand on se retrouve avec une ânerie pareille, on se dit que les types non signés, c'est vraiment mal.

    • [^] # Re: Value Semantics Rocks

      Posté par  (site web personnel) . Évalué à -4.

      Bref, pour écrire des codes performants, renvoyez par valeur.

      Ben "ça dépend", je vais citer une personne :

      vous verrez qu'avec un bon compilateur (le compilo intel, ou une version récente de gcc)

      Donc il y a une condition, ce qui casse ta phrase qui fait croire que c'est général (je vais forcer le trait : tu trompes les gens avec ta phrase qui n'a pas de "dans la condition X").
      Le problème est que tout le monde n'a pas un "bon compilateur", donc tu pénaliseras d'autres en faisant ta préférence.

      De manière générale, l'aliasing induit par ces "retour par référence" peut être catastrophique pour la performance.

      L'inverse aussi, suivant les compilos, et c'est même pour cette raison que les gens codent de cette façon.

      Bref : ce n'est pas aussi évident et pas "une seule solution pour tous" que tu l'imagines, car si, te façon de faire a un impact sur les performance, en tous cas pour plein de gens.
      C'est plus compliqué que "pour écrire des codes performants, renvoyez par valeur".

      • [^] # Re: Value Semantics Rocks

        Posté par  . Évalué à 9.

        J'ai le sentiment que tu as mal compris mes propos, voire que tu les déformes.

        • Je ne connais aucun compilateur qui ne fait pas de "return value optimization". Gcc, Clang, Visual Studio, PGI, Intel font tous ces optimisations. Comme signalé dans la news, Gcc le fait même en -O0 et il faut une option speciale pour ne pas le faire. Donc vous pouvez renvoyer des gros objets par valeur. J'ai dit dans mon post qu'il fallait cependant faire attention à une ou deux règles pour cela. Voici un premier cas où on est sur que ces compilateurs optimisent.
        U f(const T& input) {
           U output{};
        
            // n'importe quoi ensuite si tous les "codepath" contiennent un return U
        }

        Le deuxième cas est celui où le programme peut renvoyer des objets différents mais que ceux si sont tous construits sur le return.

        U f(const T& input) {
            ...
            if (...) {
                return U{...};
            } else {
                return U{...};
            }
        }

        Évidemment, un code du type:

        U f(const T& input) {
            U out0{...};
            U out1{...};
        
            if (...) {
                return out0;
            } else {
                return out1;
            }
        }

        ne peut pas bénificier de return value optimisation car à la construction des objets, on ne sait pas lequel va être renvoyé.

        • Enfin l'exemple où je disais qu'il fallait le compilo Intel ou une version récente de Gcc pour voir une accéleration de performance avec un retour par valeur a été choisi parce qu'il est simple à expliquer. Si vous prenez un compilateur qui n'est pas aussi bon, les deux codes seront simplement aussi rapides sur cet exemple. À noter qu'il y a plein d'autres cas où même une vieille version de gcc produira un code plus performant avec un retour par valeur.

        Donc je maintiens mon propos : pour la performance en C++, renvoyez par valeur.

        • [^] # Re: Value Semantics Rocks

          Posté par  . Évalué à 1.

          Je ne connais aucun compilateur qui ne fait pas de "return value optimization". Gcc, Clang, Visual Studio, PGI, Intel font tous ces optimisations.

          J’ai eu des cas avec Visual Studio où l’optimisation n’était pas faite, et donc de gros changements de perf en changeant la signature. Je ne sais plus si c’était spécifique au debug ou si c’était aussi le cas en release, mais ça rendait le code tellement lent que de toute façon, débugger devenant un enfer, il fallait faire autrement.

          Par contre, c’était il y a pas loin de 10 ans, avec Visual Studio 2003 ou 2005, je ne sais plus lequel des deux. À cette époque, passer par référence était une bonne pratique. Le langage a bien changé depuis :).

          Mes commentaires sont en wtfpl. Une licence sur les commentaires, sérieux ? o_0

          • [^] # Re: Value Semantics Rocks

            Posté par  . Évalué à 4.

            Par contre, c’était il y a pas loin de 10 ans, avec Visual Studio 2003 ou 2005, je ne sais plus lequel des deux. À cette époque, passer par référence était une bonne pratique. Le langage a bien changé depuis :).

            Oui. C'est vrai que c'est un des problèmes du C++ : les bonnes pratiques changent avec le temps.

          • [^] # Re: Value Semantics Rocks

            Posté par  (site web personnel) . Évalué à 3.

            Le langage n'a pas tant changé que cela de ce côté là. Le SunCC 5 et des bananes que j'utilisais il y a une douzaine d'année (et il était encore plus vieux que ça) m'avait surpris en sachant appliquer du NRVO sur des std::vector.

            Après, les compilateurs… oui, il y a eu de sacrés progrès (ou investissement de ressources humaines et comptables pour certains) de leur côté.

      • [^] # Re: Value Semantics Rocks

        Posté par  . Évalué à 1.

        Tu le fais exprès de faire ton drosophile qui n'apporte rien à la discussion, à part prouver que tu as lu de travers et vite fait pour t'insérer dans la "faille" du post auquel tu réponds ?!

        Ou j'sais pas moi, peut-être que le moinssage t'apporte félicité… Tiens, en voilà un de plus.

        Te voilà servi !

        "Quand certains râlent contre systemd, d'autres s'attaquent aux vrais problèmes." (merci Sinma !)

      • [^] # Re: Value Semantics Rocks

        Posté par  . Évalué à 4.

        Quelque part, si tu cherches réellement la performance à tout prix (c'est-à-dire que tu n'as pas d'autres contraintes comme la conso énergétique, la taille de code, etc.), alors payer pour le compilo Intel, ou au moins récupérer une version récente de GCC n'est quand même pas du luxe. La première règle pour avoir un code qui va vite, c'est d'utiliser un compilateur qui optimise bien.

    • [^] # Re: Value Semantics Rocks

      Posté par  . Évalué à 10.

      Le premier argument pour supprimer ce genre de signature, c'est que ce n'est pas une fonction pure. Quelque soit le langage si tu as le moyen de rendre une fonction pure fais-le ou donne d'excellent arguments vérifiés pour ne pas le faire (les « on dit » ça ne suffit pas). J'avais fais un journal là dessus il y a… 4 ans !? (putain que le temps passe !) https://linuxfr.org/users/barmic/journaux/adopter-un-style-de-programmation-fonctionnel L'article de John Carmack et les commentaires du journal sont très intéressants.

      Globalement ça t'apporte pleins de choses d'un point de vu qualitatif du code et c'est ce qui fait que les compilateurs ou les interpréteurs ont de meilleures optimisations dessus. Mais c'est pas l'avantage le plus important AMHA.

      Je trouve d'ailleurs dommage que lorsque l'on parle du C++, on parle toujours de performance extrême. Expliquer qu'un retour de fonction peut être 3 fois plus rapide c'est cool, mais c'est de la micro-optimisation. Surtout que le C++ a d'autres corde à son arc. Le premier qui me vient en tête c'est le typage qui peut être assez expressif, mais qui est original (via la programmation template).


      J'en profite pour faire une demande aux gens qui écrivent la série de dépêche sur le C++ : est-ce qu'il y aurait moyen d'avoir quelque chose sur le typage/la création d'API ? Une bonne pratique de développement est de penser tout son code comme une API notamment parce que ça améliore la testabilité. Je suis sûr qu'il y a des choses à dire là dessus pour manipuler le typage et faire des API qui réduisent les erreurs de manipulation.

      Je serais aussi intéressé par des informations sur l'écosystème. Les bibliothèques phares (boost est connu, mais une partie doit être dépréciée maintenant, non ? j'ai vu des gens parler de eggs, il y a eigen,…), les frameworks, comment on test, comment on build (make ? cmake ? autre ?), comment avoir de l'autocomplétion intélligente,…

      Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

      • [^] # Re: Value Semantics Rocks

        Posté par  . Évalué à 4. Dernière modification le 15 décembre 2016 à 09:56.

        Le premier argument pour supprimer ce genre de signature, c'est que ce n'est pas une fonction pure

        Comme mon truc, c'est le HPC je me focalise souvent sur la performance. Du coup j'en oublie l'essentiel. C'est effectivement très important de travailler avec des fonctions pures dès que c'est possible. C'est d'ailleurs pour cela que je ne suis pas fan des exceptions (même si ça peut parfois être bien pratique). Dans un monde où le multithreading et la vectorisation sont de plus en plus importantes, les fonctions pures sont encore plus importantes qu'hier. L'article de John Carmack est excellent.

        Renvoyer les objets par valeur a donc tous les avantages : côté fonctionnel et performance. Et pourtant, beaucoup résistent encore pour de sombres histoires de performance qui sont fausses et qu'ils n'ont d'ailleurs souvent jamais mesurée.

      • [^] # Re: Value Semantics Rocks

        Posté par  (site web personnel) . Évalué à 3. Dernière modification le 15 décembre 2016 à 10:21.

        En ce moment ils ont commencé une série de dépêches présentant les évolutions du C++17. Je me suis joint à la troupe pour les quelques sujets que j'avais suivis.

        Ce à quoi tu fais référence a le vent en poupe en ce moment, en particulier suite aux CppCoreGuidelines et à la bibliothèque support GSL. Il a eu de nouvelles tentatives pour faire standardiser des moyens simplifiés pour définir des types opaques en C++. Il y a eu bottage en touche pour le C++17 avec comme argument, utilisez des enum class. Je suis justement en train d'expérimenter sur le sujet, et j'ai une question d'équilibre à trouver (pour définir des positions et des distances pixelliques dans des images: faut-il vraiment deux types ? signed ou unsigned ? …)

        Je m'étais aussi posé la question de définir des types domaines de définition sur des plages réelles, avec objectif à termes de rajouter un chapitre à ma série de billets sur la Programmation par Contrat. J'ai donné le lien il y a deux commentaires de cela—qui pointe vers une autre lib pour avoir des types opaques (ou strong typedef suivant le microcosme d'où l'auteur vient). Je ne suis pas sûr que cela soit vraiment exploitable: il y a un problème de syntaxe, mais aussi un problème de précision de calculs. Il est bien beau de dire que sqrt(1+sin(x)) est toujours défini, mais en pratique 1+sin(x) pourrait être négatif car nous sommes nuls à un epsilon près.

        Dans un gist, j'ai une autre expérience avec des types opaques (sans utiliser des enum class), mais la lib publiée sur reddit est bien plus aboutie.

        EDIT: Pour les libs, il y en a des tonnes. Eigen, mais aussi blaze pour le calcul matriciel avec un syntaxe humaine. HPX qui fait de plus en plus parler de lui pour l'HPC dans le style du standard de 2011—je soupçonne que les évolutions sur la parallélisation en C++2017 viennent de ce projet. Bref. En général, c'est plus dans les communautés dédiées que je vois passer ce genre de sujets : dvpz, zds, voire sdz/oc. La complétion intelligente dépend de l'EDI. La mode est à encapsuler clang aujourd'hui. Cf p.ex. YouCompleteMe pour vim.

        • [^] # Re: Value Semantics Rocks

          Posté par  (site web personnel) . Évalué à 4.

          Je complète l’excellent commentaire de lmg HS pour indiquer que nous avons encore beaucoup de dépêches concernant C++17 à sortir, mais nous n'arrivons pas à tenir le rythme de rédaction de dépêches de qualité. Alors, n'hésitez pas à nous donner un coup de main comme lmg HS. Ne vous privez pas, on cherche à créer ces dépêches de qualité sous les noms des nouveaux contributeurs.

          De mon côté, en parallèle du C++, j'ai déjà prévue une série de dépêches sur les Bons Tests Unitaires (C++/Java/Python/Go), sur les KISS/SOLID/…, sur CMake et d'autres sujets qui n'ont pas grand chose à voir comme le danger des brevets logiciels… Si un lecteur souhaite se lancer dans un sujet qui lui semble important, mon conseil et de commencer à apprendre par lui même, de prendre des notes, de la consolider, puis de les partager avec la communauté LinuxFr.org sous la forme d'une dépêche.

          Pour ceux qui apprécient, CMake, les IDE libres et l’auto-complétion, sachez que CMake va embarquer un mode "serveur". C'est à dire que l'IDE pourra lancer ce "démon CMake" et lui envoyer des requêtes pour connaître des détails de compilation que seul CMake connaissait seul jusque là. Et c'est QtCreator-4.3 qui devrait être le premier IDE à en profiter. Conjugué aux améliorations de Clang pour la prochaine version, nous allons connaître une expérience "développeur assisté" que nous envions à nos amis Java/C#/Go/… Vive 2017 :-)

          Commentaire sous licence Creative Commons Zero CC0 1.0 Universal (Public Domain Dedication)

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.