Le C ++ moderne peut-il vous offrir des performances gratuitement?

205

Il est parfois affirmé que C ++ 11/14 peut vous aider à améliorer vos performances même lorsque vous compilez simplement du code C ++ 98. La justification se situe généralement dans le sens de la sémantique de déplacement, car dans certains cas, les constructeurs rvalue sont générés automatiquement ou font désormais partie de la STL. Maintenant, je me demande si ces cas étaient déjà réellement traités par RVO ou des optimisations de compilateur similaires.

Ma question est alors de savoir si vous pourriez me donner un exemple réel d'un morceau de code C ++ 98 qui, sans modification, s'exécute plus rapidement à l'aide d'un compilateur prenant en charge les nouvelles fonctionnalités du langage. Je comprends qu'un compilateur conforme standard n'est pas requis pour faire l'élision de copie et c'est pour cette raison que la sémantique de mouvement peut entraîner de la vitesse, mais j'aimerais voir un cas moins pathologique, si vous voulez.

EDIT: Juste pour être clair, je ne demande pas si les nouveaux compilateurs sont plus rapides que les anciens compilateurs, mais plutôt s'il y a du code par lequel ajouter -std = c ++ 14 à mes drapeaux de compilateur, il fonctionnerait plus rapidement (évitez les copies, mais si vous peut proposer autre chose que déplacer la sémantique, je serais aussi intéressé)

un grand
la source
3
N'oubliez pas que l'élision de copie et l'optimisation de la valeur de retour sont effectuées lors de la construction d'un nouvel objet à l'aide d'un constructeur de copie. Cependant, dans un opérateur d'affectation de copie, il n'y a pas d'élision de copie (comment cela peut-il être, car le compilateur ne sait pas quoi faire avec un objet déjà construit qui n'est pas temporaire). Par conséquent, dans ce cas, C ++ 11/14 gagne gros, en vous donnant la possibilité d'utiliser un opérateur d'affectation de déplacement. À propos de votre question, je ne pense pas que le code C ++ 98 devrait être plus rapide s'il est compilé par un compilateur C ++ 11/14, peut-être est-il plus rapide car le compilateur est plus récent.
vsoftco
27
Le code qui utilise la bibliothèque standard est également potentiellement plus rapide, même si vous le rendez entièrement compatible avec C ++ 98, car en C ++ 11/14, la bibliothèque sous-jacente utilise la sémantique de déplacement interne lorsque cela est possible. Ainsi, le code qui semble identique en C ++ 98 et C ++ 11/14 sera (éventuellement) plus rapide dans ce dernier cas, chaque fois que vous utilisez les objets de bibliothèque standard tels que des vecteurs, des listes, etc. et que vous déplacez la sémantique fait une différence.
vsoftco
1
@vsoftco, C'est le genre de situation à laquelle je faisais allusion, mais je n'ai pas pu trouver d'exemple: d'après ce dont je me souviens si je dois définir le constructeur de copie, le constructeur de déplacement ne sera pas généré automatiquement, ce qui nous laisse avec classes très simples où RVO, je pense, fonctionne toujours. Une exception pourrait être quelque chose en conjonction avec les conteneurs STL, où les constructeurs rvalue sont générés par l'implémenteur de bibliothèque (ce qui signifie que je n'aurais rien à changer dans le code pour qu'il utilise les déplacements).
grand
les classes n'ont pas besoin d'être simples pour ne pas avoir de constructeur de copie. C ++ prospère sur la sémantique des valeurs, et le constructeur de copie, l'opérateur d'affectation, le destructeur, etc. devrait être l'exception.
sp2danny
1
@Eric Merci pour le lien, c'était intéressant. Cependant, après l'avoir rapidement examiné, les avantages de vitesse semblent provenir principalement de l'ajout std::moveet du déplacement de constructeurs (ce qui nécessiterait des modifications du code existant). La seule chose vraiment liée à ma question était la phrase "Vous obtenez des avantages immédiats en termes de vitesse simplement en recompilant", qui n'est étayée par aucun exemple (elle mentionne STL sur la même diapositive, comme je l'ai fait dans ma question, mais rien de spécifique) ). Je demandais des exemples. Si je lis mal les diapositives, faites-le moi savoir.
Alarge

Réponses:

221

Je connais 5 catégories générales où la recompilation d'un compilateur C ++ 03 en C ++ 11 peut entraîner des augmentations de performances illimitées qui ne sont pratiquement pas liées à la qualité de l'implémentation. Ce sont toutes des variantes de la sémantique des mouvements.

std::vector réaffecter

struct bar{
  std::vector<int> data;
};
std::vector<bar> foo(1);
foo.back().data.push_back(3);
foo.reserve(10); // two allocations and a delete occur in C++03

chaque fois que le fooest réaffecté tampon de 03 en C ++ , il copié tous vectoren bar.

En C ++ 11, il déplace à la place le bar::datas, qui est fondamentalement gratuit.

Dans ce cas, cela repose sur des optimisations à l'intérieur du stdconteneur vector. Dans tous les cas ci-dessous, l'utilisation de stdconteneurs est simplement parce que ce sont des objets C ++ qui ont une movesémantique efficace en C ++ 11 "automatiquement" lorsque vous mettez à niveau votre compilateur. Les objets qui ne le bloquent pas et qui contiennent un stdconteneur héritent également des moveconstructeurs améliorés automatiques .

Échec NRVO

Lorsque NRVO (nommé l'optimisation de la valeur de retour) échoue, en C ++ 03, il retombe sur copie, sur C ++ 11, il retombe en mouvement. Les échecs de NRVO sont faciles:

std::vector<int> foo(int count){
  std::vector<int> v; // oops
  if (count<=0) return std::vector<int>();
  v.reserve(count);
  for(int i=0;i<count;++i)
    v.push_back(i);
  return v;
}

ou même:

std::vector<int> foo(bool which) {
  std::vector<int> a, b;
  // do work, filling a and b, using the other for calculations
  if (which)
    return a;
  else
    return b;
}

Nous avons trois valeurs - la valeur de retour et deux valeurs différentes dans la fonction. Elision permet de «fusionner» les valeurs de la fonction avec la valeur de retour, mais pas entre elles. Ils ne peuvent pas tous les deux être fusionnés avec la valeur de retour sans fusionner les uns avec les autres.

Le problème de base est que l'élision NRVO est fragile, et le code avec des modifications non proches du returnsite peut soudainement avoir des réductions de performances massives à cet endroit sans aucun diagnostic émis. Dans la plupart des cas d'échec NRVO, C ++ 11 se termine par un move, tandis que C ++ 03 se termine par une copie.

Renvoyer un argument de fonction

L'élision est également impossible ici:

std::set<int> func(std::set<int> in){
  return in;
}

en C ++ 11 c'est pas cher: en C ++ 03 il n'y a aucun moyen d'éviter la copie. Les arguments des fonctions ne peuvent pas être élidés avec la valeur de retour, car la durée de vie et l'emplacement du paramètre et de la valeur de retour sont gérés par le code appelant.

Cependant, C ++ 11 peut passer de l'un à l'autre. (Dans un exemple moins jouet, quelque chose pourrait être fait pour le set).

push_back ou insert

Enfin, l'élision dans les conteneurs ne se produit pas, mais les surcharges C ++ 11 rvalue déplacent les opérateurs d'insertion, ce qui économise des copies.

struct whatever {
  std::string data;
  int count;
  whatever( std::string d, int c ):data(d), count(c) {}
};
std::vector<whatever> v;
v.push_back( whatever("some long string goes here", 3) );

en C ++ 03 un temporaire whateverest créé, puis il est copié dans le vecteur v. 2 std::stringtampons sont alloués, chacun avec des données identiques, et un est rejeté.

En C ++ 11, un temporaire whateverest créé. La whatever&& push_backsurcharge est alors movetemporaire dans le vecteur v. Un std::stringtampon est alloué et déplacé dans le vecteur. Un vide std::stringest jeté.

Affectation

Volé de la réponse de @ Jarod42 ci-dessous.

L'élision ne peut pas se produire avec l'affectation, mais le déplacement peut.

std::set<int> some_function();

std::set<int> some_value;

// code

some_value = some_function();

some_functionrenvoie ici un candidat à élider, mais comme il n'est pas utilisé pour construire directement un objet, il ne peut pas être élidé. En C ++ 03, les résultats ci-dessus entraînent la copie du contenu du temporaire some_value. En C ++ 11, il est déplacé vers some_value, qui est fondamentalement gratuit.


Pour le plein effet de ce qui précède, vous avez besoin d'un compilateur qui synthétise les constructeurs de mouvements et les affectations pour vous.

MSVC 2013 implémente les constructeurs de déplacement dans des stdconteneurs, mais ne synthétise pas les constructeurs de déplacement sur vos types.

Les types contenant std::vectors et similaires n'obtiennent donc pas de telles améliorations dans MSVC2013, mais commenceront à les obtenir dans MSVC2015.

clang et gcc ont depuis longtemps implémenté des constructeurs de mouvements implicites. Le compilateur d'Intel 2013 prendra en charge la génération implicite de constructeurs de déplacement si vous réussissez -Qoption,cpp,--gen_move_operations(ils ne le font pas par défaut dans un effort de compatibilité croisée avec MSVC2013).

Yakk - Adam Nevraumont
la source
1
@alarge yes. Mais pour qu'un constructeur de déplacement soit beaucoup plus efficace qu'un constructeur de copie, il doit généralement déplacer des ressources au lieu de les copier. Sans écrire vos propres constructeurs de déplacement (et simplement recompiler un programme C ++ 03), les stdconteneurs de bibliothèque seront tous mis à jour avec les moveconstructeurs "gratuitement" et (si vous ne les avez pas bloqués) les constructions qui utilisent lesdits objets ( et lesdits objets) commenceront à obtenir une construction de mouvement libre dans un certain nombre de situations. Beaucoup de ces situations sont couvertes par élision en C ++ 03: pas toutes.
Yakk - Adam Nevraumont
5
C'est donc une mauvaise implémentation de l'optimiseur, car les objets nommés différemment renvoyés n'ont pas de durée de vie qui se chevauchent, RVO est théoriquement toujours possible.
Ben Voigt
2
@alarge Il y a des endroits où l'élision échoue, comme lorsque deux objets avec des durées de vie qui se chevauchent peuvent être élidés en un troisième, mais pas l'un pour l'autre. Ensuite, déplacer est requis en C ++ 11 et copier en C ++ 03 (en ignorant comme si). L'élision est souvent fragile dans la pratique. L'utilisation des stdconteneurs ci-dessus est principalement parce qu'ils sont peu coûteux à déplacer exoensivement pour copier le type que vous obtenez «gratuitement» en C ++ 11 lors de la recompilation de C ++ 03. C'est vector::resizeune exception: il utilise moveen C ++ 11.
Yakk - Adam Nevraumont
27
Je ne vois qu'une seule catégorie générale qui est la sémantique des mouvements, et 5 cas particuliers.
Johannes Schaub - litb
3
@sebro Je comprends que vous ne considérez pas "que les programmes n'allouent pas plusieurs milliers d'allocations de plusieurs kilo-octets et déplacent plutôt des pointeurs" comme étant suffisants. Vous voulez des résultats chronométrés. Les microbenchmarks ne sont pas plus une preuve d'amélioration des performances qu'une preuve que vous en faites fondamentalement moins. À court de quelques 100 applications du monde réel dans une grande variété d'industries profilées avec le profilage des tâches du monde réel n'est pas vraiment une preuve. J'ai pris de vagues affirmations sur les "performances gratuites" et leur ai fait des faits spécifiques sur les différences de comportement du programme sous C ++ 03 et C ++ 11.
Yakk - Adam Nevraumont
46

si vous avez quelque chose comme:

std::vector<int> foo(); // function declaration.
std::vector<int> v;

// some code

v = foo();

Vous avez obtenu une copie en C ++ 03, tandis que vous avez obtenu une affectation de déplacement en C ++ 11. vous avez donc une optimisation gratuite dans ce cas.

Jarod42
la source
4
@Yakk: Comment l'élision de copie se produit-elle dans l'affectation?
Jarod42
2
@ Jarod42 Je crois également que la suppression de copie n'est pas possible dans une affectation, car le côté gauche est déjà construit et il n'y a aucun moyen raisonnable pour un compilateur de savoir quoi faire avec les "anciennes" données après avoir volé les ressources de la droite côté. Mais peut-être que je me trompe, j'aimerais trouver une fois pour toujours la réponse. L'élision de copie a un sens lorsque vous copiez une construction, car l'objet est "frais" et il n'y a aucun problème à décider quoi faire avec les anciennes données. Pour autant que je sache, la seule exception est la suivante: "Les affectations ne peuvent être éludées que sur la base de la règle"
vsoftco
4
Bon code C ++ 03 a déjà fait un pas dans ce cas, viafoo().swap(v);
Ben Voigt
@BenVoigt bien sûr, mais tout le code n'est pas optimisé, et tous les endroits où cela se produit ne sont pas faciles à atteindre.
Yakk - Adam Nevraumont
L'ellision de copie peut fonctionner dans une affectation, comme le dit @BenVoigt. Le meilleur terme est RVO (optimisation de la valeur de retour) et ne fonctionne que si foo () a été implémenté comme ça.
DrumM