199 votes

c++11 Optimisation ou déplacement de la valeur de retour ?

Je ne comprends pas quand je dois utiliser std::move et quand je dois laisser le compilateur optimiser... par exemple :

using SerialBuffer = vector< unsigned char >;

// let compiler optimize it
SerialBuffer read( size_t size ) const
{
    SerialBuffer buffer( size );
    read( begin( buffer ), end( buffer ) );
    // Return Value Optimization
    return buffer;
}

// explicit move
SerialBuffer read( size_t size ) const
{
    SerialBuffer buffer( size );
    read( begin( buffer ), end( buffer ) );
    return move( buffer );
}

Lequel dois-je utiliser ?

0 votes

D'après ce que j'ai lu jusqu'à présent, le consensus général semble compter sur le fait que le compilateur utilise RVO au lieu de move explicitement : les compilateurs modernes sont suffisamment intelligents pour utiliser le RVO presque partout et il est plus efficace que le RVO. move . Mais ce ne sont que des "ouï-dire", alors je suis tout à fait intéressé par une explication documentée.

12 votes

Vous n'avez jamais besoin de déplacer explicitement la valeur de retour d'une fonction de variable locale. C'est un déplacement implicite.

3 votes

Le compilateur est alors libre de choisir : Si c'est possible, il utilisera RVO et si ce n'est pas le cas, il peut toujours faire un déplacement (et si aucun déplacement n'est possible pour le type, alors il fera une copie).

137voto

Kerrek SB Points 194696

Utilisez exclusivement la première méthode :

Foo f()
{
  Foo result;
  mangle(result);
  return result;
}

Cela permettra déjà permettre l'utilisation du constructeur de déplacement, s'il en existe un. En fait, une variable locale peut se lier à une référence rvalue dans une fonction return précisément quand l'élision de copie est autorisée.

Votre deuxième version interdit activement l'élision de la copie. La première version est universellement meilleure.

1 votes

Même lorsque l'élision de copie est désactivée ( -fno-elide-constructors ), le constructeur de mouvement est appelé.

0 votes

@Maggyero : -fno-elide-constructors ne désactive pas l'élision de la copie, elle désactive l'optimisation de la valeur de retour. La première est une règle du langage que vous ne pouvez pas "désactiver" ; la seconde est une optimisation qui tire parti de cette règle. En fait, je voulais dire que même si l'optimisation de la valeur de retour n'est pas utilisée, vous pouvez toujours utiliser la sémantique de déplacement, qui fait partie du même ensemble de règles de langage.

0 votes

Documentation GCC en -fno-elide-constructors : " La norme C++ permet à une implémentation d'omettre la création d'un temporaire qui n'est utilisé que pour initialiser un autre objet du même type ". La spécification de cette option désactive cette optimisation, et force G++ à appeler le constructeur de copie dans tous les cas. Cette option oblige également G++ à appeler des fonctions membres triviales qui, autrement, seraient développées en ligne. En C++17, le compilateur est tenu d'omettre ces temporaires, mais cette option affecte toujours les fonctions membres triviales."

129voto

Jamin Grey Points 2323

Toutes les valeurs de retour sont soit déjà moved ou optimisé, il n'est donc pas nécessaire de se déplacer explicitement avec les valeurs de retour.

Les compilateurs sont autorisés à déplacer automatiquement la valeur de retour (pour optimiser la copie), et même à optimiser le déplacement !

Section 12.8 du projet de norme n3337 (C++11) :

Lorsque certains critères sont remplis, une implémentation est autorisée à omettre la construction de copie/déplacement d'un objet de classe, même si la construction de copie/déplacement et/ou le destructeur de l'objet ont des effets secondaires. Dans Dans de tels cas, l'implémentation traite la source et la cible de l'objet de classe comme des effets secondaires. l'opération de copier/déplacer omise comme étant simplement deux façons différentes de se référer à au même objet, et la destruction de cet objet se produit à la dernière destruction de cet objet a lieu au dernier moment où les deux objets auraient été détruits sans l'optimisation. Cette élision des opérations de copie/déplacement, appelée élision de la copie est autorisé dans les circonstances suivantes (qui peuvent être combinés pour éliminer les copies multiples) :

[...]

Ejemplo :

class Thing {
public:
Thing();
   ~Thing();
   Thing(const Thing&);
};

Thing f() {
   Thing t;
   return t;
}

Thing t2 = f();

Ici, les critères d'élision peuvent être combinés pour éliminer deux appels au constructeur de copie de la classe Thing : la copie de l'objet automatique local t dans l'objet temporaire pour la valeur de retour de la fonction f() et la copie de cet objet temporaire dans l'objet t2 . En effet, la construction de l'objet local t peut être vu comme initialisant directement l'objet global t2 et la destruction de cet objet aura lieu au moment du programme du programme. L'ajout d'un constructeur de déplacement à Thing a le même effet, mais il s'agit de la construction du mouvement à partir de l'élément objet temporaire vers t2 qui est élidée. - exemple de fin ]

Lorsque les critères d'exclusion d'une opération de copie sont remplis ou le seraient si ce n'était que la source est un paramètre de fonction, et que l'objet à copier est désigné par une lvalue, la résolution de surcharge pour sélectionner le constructeur de la copie est d'abord effectuée comme si l'objet était désigné par une rvalue. pour sélectionner le constructeur de la copie est d'abord effectuée comme si l'objet était désigné par une rvalue. Si la résolution de surcharge échoue, ou si le type du premier paramètre du constructeur sélectionné n'est pas une référence rvalue à la fonction référence rvalue au type de l'objet (éventuellement qualifiée cv), la résolution de surcharge est à nouveau effectuée, en considérant l'objet comme une lvalue. lvalue.

14 votes

Je n'aime pas particulièrement l'argument "les compilateurs peuvent faire X". La question ne nécessite pas le recours à un quelconque compilateur. Il s'agit uniquement du langage. Et il n'y a rien d'"optionnel" ou de vague dans le fait de savoir si "un mouvement" se produit. Le langage est parfaitement clair sur les types de paramètres de constructeur qui peuvent se lier à la valeur de retour (qui est une xvalue) ; la résolution de surcharge fait le reste.

5 votes

Il ne s'agit pas de ce que les compilateurs peuvent faire, mais de ce que les principaux compilateurs faire faire. Déplacer les choses explicitement pourrait empêcher les compilateurs de faire des choses encore mieux que de les déplacer. Tout compilateur qui est assez avancé pour vous permettre de déplacer explicitement est presque certainement assez avancé pour déplacer automatiquement les valeurs de retour - parce que contrairement à d'autres situations où vous pourriez vouloir déplacer explicitement, la valeur de retour est très facile à détecter pour les compilateurs comme un bon endroit à optimiser (parce que tout retour est une garantie que la valeur ne sera pas utilisée plus loin dans la fonction qui fait le retour).

0 votes

@KerrekSB, "... Le langage est parfaitement clair sur les types de paramètres de constructeur..." cela suppose qu'un constructeur doit être appelé, et nous discutons simplement de quel constructeur sera appelé. Mais dans le cas de RVO, il n'y a pas besoin d'appeler un constructeur au moment du retour. Je pense que c'est le problème du "facultatif".

76voto

Oktalist Points 2524

C'est très simple.

return buffer;

Si vous faites cela, alors soit la NRVO se produira, soit elle ne se produira pas. Si elle ne se produit pas, alors buffer sera déplacé.

return std::move( buffer );

Si vous faites cela, alors NVRO ne sera pas se produisent, et buffer sera déplacée.

Il n'y a donc rien à gagner à utiliser std::move ici, et beaucoup à perdre.


Il y a une exception* à la règle ci-dessus :

Buffer read(Buffer&& buffer) {
    //...
    return std::move( buffer );
}

Si buffer est une référence rvalue, alors vous devriez utiliser std::move . Cela s'explique par le fait que les références ne sont pas éligibles pour le NRVO, donc sans std::move il en résulterait une copie à partir d'une lvalue.

Il s'agit juste d'un exemple de la règle "toujours move Références aux valeurs r et forward références universelles", qui prévaut sur la règle règle "ne jamais move une valeur de retour".

* À partir de C++20, cette exception peut être oubliée. Les références Rvalue dans return Les déclarations sont implicitement déplacées, maintenant.

31voto

Adam H. Peterson Points 3252

Si vous retournez une variable locale, n'utilisez pas move() . Cela permettra au compilateur d'utiliser NRVO, et à défaut, le compilateur sera toujours autorisé à effectuer un déplacement (les variables locales deviennent des valeurs R au sein d'une return ). Utilisation de move() dans ce contexte inhiberait simplement NRVO et forcerait le compilateur à utiliser un déplacement (ou une copie si le déplacement n'est pas disponible). Si vous retournez quelque chose d'autre qu'une variable locale, NRVO n'est de toute façon pas une option et vous devriez utiliser la fonction move() si (et seulement si) vous avez l'intention de chaparder l'objet.

0 votes

Est-ce correct ? Si je réutilise l'exemple de : fr.cppreference.com/w/cpp/language/copy_elision L'ajout d'un std::move (ligne 17) sur la déclaration de retour, ne désactive pas l'élision de copie. La norme dit en fait que l'élision de copie omettra "std::move" et les constructeurs de copie.

0 votes

@ThomasLegris, je ne comprends pas votre commentaire. Si vous parlez de return v; sous cette forme, NRVO élide le déplacement (et la copie). Sous C++14, il n'était pas nécessaire d'effectuer l'élidage du déplacement, mais il était nécessaire d'effectuer l'élidage de la copie (nécessaire pour supporter les types à déplacement seul). Je crois que dans les normes C++ plus récentes, il est nécessaire d'élider le déplacement aussi (pour supporter les types immobiles). Si la ligne est plutôt return std::move(v); vous ne retournez plus une variable locale ; vous retournez une expression, et NRVO n'est pas admissible --- un déplacement (ou une copie) sera nécessaire.

0 votes

Il semble que les compilateurs soient assez intelligents pour supprimer l'élément std::move et appliquer le NRVO. Ajout de return std::move(v); en ligne 17 montre empiriquement que ni le constructeur move ni le constructeur copy ne sont jamais appelés (Vous pouvez essayer en cliquant sur "run it" et en sélectionnant l'option de compilation "gcc 4.7 C++11"). Clang, quant à lui, émet un avertissement mais est toujours capable d'appliquer NRVO. Donc je suppose que c'est une très bonne pratique de ne pas ajouter std::move mais l'ajouter ne va pas nécessairement inhiber purement le NRVO, c'est ce que je voulais dire.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X