Partage
  • Partager sur Facebook
  • Partager sur Twitter

Je ne comprends pas l'utilité de std::move

    6 novembre 2021 à 20:58:10

    Salut tout le monde !

    Je n'arrive pas à comprendre en quoi utiliser des rvalues plutôt que des lvalues entraîne un gain de performance. Et je ne comprends pas non plus la nuance entre utiliser une lvalue et une rvalue en argument d'une fonction. Les deux sont des valeurs non ? Donc même si ces valeurs sont utilisées dans des contextes différents (ça j'ai bien réussi à le comprendre) en quoi les rvalues sont plus efficaces ?

    Quelqu'un pourrait m'expliquer svp ?

    Merci d'avance !

    -
    Edité par Autechre 6 novembre 2021 à 20:58:33

    • Partager sur Facebook
    • Partager sur Twitter
      6 novembre 2021 à 21:29:49

      Les rvalues sont plus efficaces lorsque le but est de transférer les valeurs d'une variable A vers une variable B et à la condition que l'objet est un comportement spécialisé.

      Par exemple, si j'ai un std::vectror<int> de 10 entiers, faire v2 = v1 va copier les valeurs de v1 dans celle de v2 (allocation de 10 entiers puis copie un à un). Alors que v2 = std::move(v1) va déplacer la propriété des valeurs de v1 dans v2 (grosso modo copie de 3 pointeurs). Ici, il n'y a pas de nouvelle allocation, l'opération est beaucoup plus rapide.

      Mais il ne faut pas utiliser std::move() pour tout et n'importe quoi, seulement quand on veut déplacer le contenu d'une variable. C’est-à-dire lorsqu'on est sûr de ne plus l'utiliser. Il faut aussi être bien conscient que seules les références ont besoin d'être déplacées, utiliser std::move sur autre chose qu'une référence / variable va légèrement dégrader les performances (les compilateurs peuvent le détecter et emmètre un avertissement).

      J'avais écrit ça sur le sujet: https://jonathanpoelen.github.io/2021/05/la-semantique-de-deplacement/ et il y a aussi un cours dédié sur zeste de savoir.

      • Partager sur Facebook
      • Partager sur Twitter
        7 novembre 2021 à 15:52:22

        Une r-value et une l-value sont comme tu l'écris toutes les deux des values. Une n'est pas plus optimale que l'autre.

        Ce qui est optimisable c'est de passer par référence une x-value.
        Par exemple, si on a une fonction qui reçoit une ( Entity&& ), on lui passe donc une référence à une x-value qui est:
        - soit une r-value
        - soit une l-value castée en x-value (c'est que que fait le std::move())
        La fonction sait donc qu'elle a droit d'altérer ce qu'elle a reçu par référence (la r-value est détruite à la fin de l'expression, et il ne faut plus utiliser la l-value castée dans la suite du code sauf pour la détruire.)
        Elle va en profiter pour piquer à l'entité ses éléments. S'ils sont simples (comme un int), il n'y a pas de gain. Mais s'il sont complexes (comme une zone dynamiquement allouée), on en va prendre la possession sans refaire une allocation ni en recopier tout le contenu.

        Par exemple, l'operateur égal de toutes les collections est optimisé pour ce cas. Si on fait :

        std::vector<double>  v = std::vector<double>( 1000, 42. );
        std::vector<double>  w = std::move(v);

        Les mille éléments qui valent 42 ne sont jamais recopiés. w a récupéré la zone de v qui l'a lui-même récupérée de la r-value std::vector<double>(1000,42.).
        En fait, le compilateur peut tout à fait optimiser le code précédent en utilisant la return value optimisation pour la première ligne et voir qu'en fait w c'est v. Donc ça n'est pas toujours utile, mais le compilateur ne peut pas être finaud à tous les coups.
        Et il existe des tas de cas où ce principe est utilisé sans que nous en ayons conscience (par exemple à chaque fois que nous utilisons des std::string). Les cas où ça provoque une perte de performance existent, ils sont extrêmement rares et la perte est très petite.

        -
        Edité par Dalfab 7 novembre 2021 à 15:56:49

        • Partager sur Facebook
        • Partager sur Twitter

        En recherche d'emploi.

          8 décembre 2021 à 1:03:19

          Ah oui ok ! En fait je pense que pour comprendre l'utilité de std::move il faut vraiment avoir des exemples précis.

          En gros, std::move sert à éviter d'utiliser l'allocation dynamique de mémoire. Hors de ce cas d'utilisation, l'utilisation de std::move est contre-productive. Par exemple, j'imagine que copier des entiers à l'intérieur d'un tableau n'est pas une action qui peut être optimisée par std::move.

          • Partager sur Facebook
          • Partager sur Twitter
            8 décembre 2021 à 1:52:25

            Peut être que "contre-productive" est trop fort. Au pire, c'est pas un gain de performance (le move fait une copie quand il n'y a pas un move spécifique)

            Par exemple, pour reprendre un code inspiré de celui de Dalfab :

            int i = 123;
            int j = std::move(i);

            fera une copie de la valeur de i vers j.

            ATTENTION : même si en pratique le compilateur fait une copie, ca reste invalide d'utiliser une variable après move (la variable i) !

            ---------------------------------------------------------------------------------------

            (Spoiler : grosse digression sur les sémantiques !)

            Pour autant, il ne faut pas oublier (ou il faut savoir) que move n'est pas juste pour l'optimisation. C'est aussi (et avant tout, de mon point de vue) un outil qui permet d'expliciter la sémantique, c'est a dire qui permet aux devs d'exprimer explicitement leurs intentions. (Et pouvoir exprimer et connaître les intentions des devs, c'est un point important pour la qualité du code et diminuer le risque d'erreur).

            Pour être concret et explicite :

            auto r = get_ressource();
            auto s = r;

            Dans le code précédent, le dev exprime l'idée qu'il veut copier la ressource, en disant "moi j'ai ma ressource et je m'occupe d'elle. Et toi, tu as ta propre ressource et tu t'occupe d'elle". (En multithreads, c'est le plus simple a gérer)

            auto r = get_ressource();
            auto& s = r; 

            (ou avec un pointeur nu, ou weak_ptr, ou autre, il y a plusieurs moyens d'exprimer une indirection)

            Dans le code précédent, le dev exprime l'idée qu'il prête l'accès à une ressource, mais en garde la responsabilité. Pour dire en quelque sorte "tiens, tu peux utiliser ma ressource, mais c'est la mienne !"

            auto r = get_ressource();
            auto s = std::move(r);

            Ici, le dev exprime qu'il donne la responsabilité de la ressource et ne s'en occupe plus. Donc "tiens, voila la ressource, c'est a toi maintenant, tu fais ce que tu en veux, ce n'est plus mon problème".

            (j'ai volontairement utilisé "auto" pour ne pas entrer dans les détails syntaxiques. Ce qui est important, c'est de comprendre ces 3 sémantiques : la copie, le partage (indirection) et le déplacement (transfert)).

            En pratique, ca ne sera pas des codes aussi simples, tu auras par exemple r et s qui seront dans des fonctions, classes, threads différents. Et potentiellement du code écrit par des devs différents.

            Dans un langage sans ces sémantiques (par exemple en C avec les pointeurs), pour savoir ce que tu dois faire (par exemple le dev qui écrit le code qui utilise s), il faut aller voir le code appelant ou la doc, pour savoir s'il doit libérer la ressource ou pas. Et réciproquement pour celui qui écrit le code qui utilise r. Et s'il y a pleins d'appels de fonctions, ça peut devenir très compliqué.

            Le but d'avoir une sémantique explicite, c'est de savoir localement, dans le contexte d'utilisation, ce que tu dois faire de la ressource. Si tu as par exemple un fonction qui prendre un unique_ptr<Ressource>, tu n'as pas besoin d'aller voir le code de cette fonction pour savoir que cette fonction prend la responsabilité de la ressource. Pas besoin d'aller lire le code de cette fonction ou la doc.

            Avoir des sémantiques riches et expressives, c'est donc un bon moyen de simplifier le code (en le rendant plus lisible) et de le rendre plus safe (en sachant ce qu'on doit/peut/ne peut pas faire)

            -
            Edité par gbdivers 8 décembre 2021 à 1:54:43

            • Partager sur Facebook
            • Partager sur Twitter
              8 décembre 2021 à 8:49:34

              Techniquement std::move est mal nommé. Certains dans la communauté du C++ auraient préféré que ça s'appelle rvalue_cast ou quelque chose du genre. En tant que telle la fonction ne fait pas de déplacement. Elle permet simplement de changer la catégorie d'un objet et donc de pouvoir appeler une autre surcharge de fonction.

              • Partager sur Facebook
              • Partager sur Twitter

              git is great because Linus did it, mercurial is better because he didn't.

                8 décembre 2021 à 10:53:08

                std::move est le comportement de base auquel on s’attend, à savoir copier des valeurs d’un endroit à un autre. Si la valeur est un pointeur, on se retrouve naturellement avec deux variables qui pointent vers la même zone, ce qui est classique dans tous les langages. Mais ce n’est pas comme ça que le C++ voit la copie. En C++ les copies sont des deep copie par défaut. std::move permet de faire une copie superficielle, en supposant qu’on ait plus besoin de la variable originale (sinon on a deux variables différentes qui pointent vers les mêmes données en mémoire, ce qui enfreint la définition de copie du C++). bref on est ici à un niveau sémantique plus élevé, ce n’est pas l’explication "bas niveau" de ce qui se passe qui permettra de comprendre la notion
                • Partager sur Facebook
                • Partager sur Twitter
                  8 décembre 2021 à 12:03:00

                  Salut,

                  Sur un exemple concret, imaginons une classe string :

                  class string
                  {
                    char* ch;
                    int taille;
                  }
                  

                  Un exercice classique, tu veux mettre une chaine dedans, tu alloc ch, tu remplis. et tu remplis taille.

                  Maintenant, si tu copies cette chaine, il y aura une autre allocation.

                  Mais si tu move une chaine c1 dans une autre c2, ça swap juste les pointeurs c1.ch et c2.ch, ainsi que ch1.taille et ch2.taille. Pas d'allocation, pas de désallocation.

                  Par contre, on a fait un move de c1 vers c2, c2 contient donc ce que c1 contenant ;  mais c1 devient une chaine vide valide. 

                  • Partager sur Facebook
                  • Partager sur Twitter

                  Recueil de code C et C++  http://fvirtman.free.fr/recueil/index.html

                    8 décembre 2021 à 12:09:46

                    Euh. Non, tout est shallow/superficiel par défaut. Cela peut devenir deep/profond quand il y a une abstraction qui est responsable d'une ressource, et que les développeurs de la capsule/abstraction ont envisagé la duplication de la ressource encapsulée d'une capsule à une autre. C'est une sémantique qui est naturelle pour toutes les structures de données, ce qui fait que c'est ce que font les conteneurs standards. Sur quantité de  ressources autre que la mémoire cela n'a aucun sens (et c'est pour ça que streams, mutex, threads... ne se dupliquent pas)

                    Quant à move(), il permet d'autoriser le transfert de responsabilité de ressource depuis une capsule responsable (*) qui est une variable locale en la déguisant en rvalue.

                    (*) a condition bien évidemment que les dev de la capsule ont implémenté le transfert de responsabilité sur la ressource encapsulée.

                    -
                    Edité par lmghs 8 décembre 2021 à 12:10:43

                    • Partager sur Facebook
                    • Partager sur Twitter
                    C++: Blog|FAQ C++ dvpz|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS| Bons livres sur le C++| PS: Je ne réponds pas aux questions techniques par MP.

                    Je ne comprends pas l'utilité de std::move

                    × Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
                    × Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
                    • Editeur
                    • Markdown