L'utilisation de l'auto de C ++ 11 peut-elle améliorer les performances?

230

Je peux voir pourquoi le autotype en C ++ 11 améliore l'exactitude et la maintenabilité. J'ai lu que cela peut également améliorer les performances ( presque toujours automatique par Herb Sutter), mais je manque une bonne explication.

  • Comment puis auto améliorer les performances?
  • Quelqu'un peut-il donner un exemple?
DaBrain
la source
5
Voir herbsutter.com/2013/06/13/… qui parle d'éviter les conversions implicites accidentelles, par exemple d'un gadget à un widget. Ce n'est pas un problème courant.
Jonathan Wakely du
42
Acceptez-vous «rend moins susceptible de pessimiser involontairement» une amélioration des performances?
5gon12eder
1
Performances du nettoyage de code à l'avenir uniquement, peut
Croll
Nous avons besoin d'une réponse courte: non si vous êtes bon. Il peut empêcher les erreurs «noobish». Le C ++ a une courbe d'apprentissage qui tue ceux qui ne le font pas après tout.
Alec Teal

Réponses:

309

autopeut améliorer les performances en évitant les conversions implicites silencieuses . Un exemple que je trouve convaincant est le suivant.

std::map<Key, Val> m;
// ...

for (std::pair<Key, Val> const& item : m) {
    // do stuff
}

Vous voyez le bug? Nous voici, pensant que nous prenons élégamment chaque élément de la carte par référence const et utilisons la nouvelle expression range-for pour clarifier notre intention, mais en réalité nous copions chaque élément. En effet , std::map<Key, Val>::value_typeest std::pair<const Key, Val>, non std::pair<Key, Val>. Ainsi, lorsque nous avons (implicitement):

std::pair<Key, Val> const& item = *iter;

Au lieu de prendre une référence à un objet existant et de le laisser là, nous devons faire une conversion de type. Vous êtes autorisé à prendre une référence const à un objet (ou temporaire) d'un type différent tant qu'une conversion implicite est disponible, par exemple:

int const& i = 2.0; // perfectly OK

La conversion de type est une conversion implicite autorisée pour la même raison que vous pouvez convertir un const Keyen un Key, mais nous devons construire un temporaire du nouveau type afin de permettre cela. Ainsi, effectivement, notre boucle:

std::pair<Key, Val> __tmp = *iter;       // construct a temporary of the correct type
std::pair<Key, Val> const& item = __tmp; // then, take a reference to it

(Bien sûr, il n'y a pas réellement d' __tmpobjet, il est juste là pour l'illustration, en réalité le temporaire sans nom est juste lié à itemsa durée de vie).

Je change simplement en:

for (auto const& item : m) {
    // do stuff
}

vient de nous sauver une tonne de copies - maintenant le type référencé correspond au type d'initialisation, donc aucun temporaire ou conversion n'est nécessaire, nous pouvons simplement faire une référence directe.

Barry
la source
19
@Barry Pouvez-vous expliquer pourquoi le compilateur ferait volontiers des copies au lieu de se plaindre d'essayer de traiter un std::pair<const Key, Val> const &comme un std::pair<Key, Val> const &? Nouveau sur C ++ 11, je ne sais pas comment le range-for et autojoue dans cela.
Agop
@Barry Merci pour l'explication. C'est le morceau qui me manquait - pour une raison quelconque, je pensais que vous ne pouviez pas avoir une référence constante à un temporaire. Mais bien sûr, vous pouvez - cela cessera d'exister à la fin de sa portée.
Agop
@barry Je vous comprends, mais le problème est qu'il n'y a pas de réponse qui couvre toutes les raisons d'utiliser autocette augmentation des performances. Je vais donc l'écrire dans mes propres mots ci-dessous.
Yakk - Adam Nevraumont
38
Je ne pense toujours pas que ce soit la preuve que " autoaméliore les performances". C'est juste un exemple qui " autoaide à prévenir les erreurs de programmation qui détruisent les performances". Je soutiens qu'il existe une distinction subtile mais importante entre les deux. Pourtant, +1.
Courses de légèreté en orbite du
70

Parce que autodéduit le type de l'expression d'initialisation, aucune conversion de type n'est impliquée. Combiné avec des algorithmes basés sur des modèles, cela signifie que vous pouvez obtenir un calcul plus direct que si vous deviez créer un type vous-même - surtout lorsque vous traitez avec des expressions dont vous ne pouvez pas nommer le type!

Un exemple typique vient de (ab) en utilisant std::function:

std::function<bool(T, T)> cmp1 = std::bind(f, _2, 10, _1);  // bad
auto cmp2 = std::bind(f, _2, 10, _1);                       // good
auto cmp3 = [](T a, T b){ return f(b, 10, a); };            // also good

std::stable_partition(begin(x), end(x), cmp?);

Avec cmp2et cmp3, l'ensemble de l'algorithme peut aligner l'appel de comparaison, alors que si vous construisez un std::functionobjet, non seulement l'appel ne peut pas être aligné, mais vous devez également passer par la recherche polymorphe dans l'intérieur effacé du type de l'encapsuleur de fonction.

Une autre variante de ce thème est que vous pouvez dire:

auto && f = MakeAThing();

Il s'agit toujours d'une référence, liée à la valeur de l'expression d'appel de fonction, et ne construit jamais d'objets supplémentaires. Si vous ne connaissiez pas le type de la valeur renvoyée, vous pourriez être contraint de construire un nouvel objet (peut-être comme temporaire) via quelque chose comme T && f = MakeAThing(). (De plus, auto &&fonctionne même lorsque le type de retour n'est pas mobile et que la valeur de retour est une valeur.)

Kerrek SB
la source
C'est donc une raison "d'éviter l'effacement de type" auto. Votre autre variante est "éviter les copies accidentelles", mais a besoin d'être embellie; pourquoi autovous donne-t-il de la vitesse à taper simplement le type là-bas? (Je pense que la réponse est "vous vous trompez de type et il se transforme silencieusement"). Ce qui en fait un exemple moins bien expliqué de la réponse de Barry, non? C'est-à-dire qu'il existe deux cas de base: auto pour éviter l'effacement de type et auto pour éviter les erreurs de type silencieuses qui se convertissent accidentellement, qui ont toutes deux un coût d'exécution.
Yakk - Adam Nevraumont
2
"non seulement l'appel ne peut pas être intégré" - pourquoi alors? Voulez-vous dire qu'en principe, quelque chose empêche l'appel d'être dévirtualisé après l'analyse du flux de données si les spécialisations pertinentes de std::bind, std::functionet std::stable_partitionont toutes été intégrées? Ou tout simplement qu'en pratique, aucun compilateur C ++ ne s'alignera de manière suffisamment agressive pour régler le désordre?
Steve Jessop
@SteveJessop: Surtout ce dernier - après avoir traversé le std::functionconstructeur, il sera très complexe de voir à travers l'appel réel, en particulier avec les optimisations de petites fonctions (donc vous ne voulez pas réellement de dévirtualisation). Bien sûr, en principe, tout est comme si ...
Kerrek SB
41

Il existe deux catégories.

autopeut éviter l'effacement des caractères. Il existe des types innommables (comme les lambdas) et des types presque innommables (comme le résultat de std::bindou d'autres choses comme des modèles d'expression).

Sans auto, vous finissez par taper effacer les données jusqu'à quelque chose comme std::function. L'effacement de type a un coût.

std::function<void()> task1 = []{std::cout << "hello";};
auto task2 = []{std::cout << " world\n";};

task1a une surcharge d'effacement de type - une allocation de tas possible, une difficulté à l'intégrer et une surcharge d'appel de table de fonction virtuelle. task2n'en a pas. Les Lambdas ont besoin d'une déduction automatique ou d'une autre forme de type pour stocker sans effacement de type; d'autres types peuvent être si complexes qu'ils n'en ont besoin qu'en pratique.

Deuxièmement, vous pouvez vous tromper de types. Dans certains cas, le mauvais type fonctionnera parfaitement, mais provoquera une copie.

Foo const& f = expression();

compilera si expression()renvoie Bar const&ou Barou même Bar&, d'où Foopeut être construit Bar. Un temporaire Foosera créé, puis lié à f, et sa durée de vie sera prolongée jusqu'à ce qu'il fdisparaisse.

Le programmeur a peut-être voulu Bar const& fy faire une copie, mais pas l'intention, mais une copie est faite malgré tout.

L'exemple le plus courant est le type de *std::map<A,B>::const_iterator, ce qui n'est std::pair<A const, B> const&pas le cas std::pair<A,B> const&, mais l'erreur est une catégorie d'erreurs qui coûte silencieusement les performances. Vous pouvez construire un à std::pair<A, B>partir d'un std::pair<const A, B>. (La clé sur une carte est const, car la modifier est une mauvaise idée)

@Barry et @KerrekSB ont tout d'abord illustré ces deux principes dans leurs réponses. Il s'agit simplement d'une tentative de mettre en évidence les deux questions dans une seule réponse, avec une formulation qui vise le problème plutôt que d'être centrée sur l'exemple.

Yakk - Adam Nevraumont
la source
9

Les trois réponses existantes donnent des exemples où l'utilisation de l' autoaide «rend moins susceptible de pessimiser involontairement» ce qui en fait «améliore les performances».

Il y a un revers à la médaille. L'utilisation autoavec des objets dont les opérateurs ne renvoient pas l'objet de base peut entraîner un code incorrect (toujours compilable et exécutable). Par exemple, cette question demande comment l'utilisation a autodonné des résultats différents (incorrects) en utilisant la bibliothèque Eigen, c'est -à- dire les lignes suivantes

const auto    resAuto    = Ha + Vector3(0.,0.,j * 2.567);
const Vector3 resVector3 = Ha + Vector3(0.,0.,j * 2.567);

std::cout << "resAuto = " << resAuto <<std::endl;
std::cout << "resVector3 = " << resVector3 <<std::endl;

a donné lieu à une sortie différente. Certes, cela est principalement dû à l'évaluation paresseuse d'Eigens, mais ce code est / devrait être transparent pour l'utilisateur (de la bibliothèque).

Bien que les performances auton'aient pas été grandement affectées ici, l'utilisation pour éviter la pessimisation involontaire peut être classée comme une optimisation prématurée, ou du moins erronée;).

Avi Ginsburg
la source
1
Ajouté la question opposée: stackoverflow.com/questions/38415831/…
Leon