"L'optimisation prématurée est la racine de tout Mal"
Je pense que nous pouvons tous nous mettre d'accord. Et j'essaie très fort d'éviter de faire ça.
Mais récemment, je me suis interrogé sur la pratique de passer des paramètres par référence const plutôt que par valeur . J'ai appris / appris que les arguments de fonction non triviaux (c'est-à-dire la plupart des types non primitifs) devraient de préférence être passés par référence const - un certain nombre de livres que j'ai lus recommandent cela comme une "meilleure pratique".
Je ne peux pas m'empêcher de me demander: les compilateurs modernes et les nouvelles fonctionnalités de langage peuvent faire des merveilles, donc les connaissances que j'ai apprises peuvent très bien être obsolètes, et je n'ai jamais vraiment pris la peine de profiler s'il y a des différences de performances entre
void fooByValue(SomeDataStruct data);
et
void fooByReference(const SomeDataStruct& data);
La pratique que j'ai apprise - passer des références const (par défaut pour les types non triviaux) - est-elle une optimisation prématurée?
la source
Réponses:
"Optimisation prématurée" ne consiste pas à utiliser les optimisations au début . Il s'agit d'optimiser avant que le problème soit compris, avant que le runtime soit compris, et de rendre souvent le code moins lisible et moins maintenable pour des résultats douteux.
Utiliser "const &" au lieu de passer un objet par valeur est une optimisation bien comprise, avec des effets bien compris sur l'exécution, sans pratiquement aucun effort, et sans aucun mauvais effet sur la lisibilité et la maintenabilité. Il améliore réellement les deux, car il me dit qu'un appel ne modifiera pas l'objet transmis. Donc, ajouter "const &" à droite lorsque vous écrivez le code n'est PAS PRÉMATURÉ.
la source
const&
, donc je pense que la question est assez sensible.const& foo
dit que la fonction ne modifiera pas foo, donc l' appelant est en sécurité. Mais une valeur copiée indique qu'aucun autre thread ne peut changer foo, donc l' appelé est en sécurité. Droite? Ainsi, dans une application multi-thread, la réponse dépend de l'exactitude et non de l'optimisation.TL; DR: passer par référence const est toujours une bonne idée en C ++, tout bien considéré. Pas une optimisation prématurée.
TL; DR2: La plupart des adages n'ont pas de sens, jusqu'à ce qu'ils le fassent.
Objectif
Cette réponse essaie juste d'étendre un peu l'élément lié sur les directives de base C ++ (mentionné pour la première fois dans le commentaire d'Amon).
Cette réponse n'essaie pas d'aborder la question de savoir comment penser et appliquer correctement les divers adages qui ont été largement diffusés dans les cercles des programmeurs, en particulier la question de la réconciliation entre des conclusions ou des preuves contradictoires.
Applicabilité
Cette réponse s'applique uniquement aux appels de fonction (étendues imbriquées non détachables sur le même thread).
(Note latérale.) Lorsque des choses passables peuvent échapper à la portée (c'est-à-dire avoir une durée de vie qui dépasse potentiellement la portée externe), il devient plus important de satisfaire le besoin de l'application de gestion de la durée de vie des objets avant toute autre chose. Habituellement, cela nécessite l'utilisation de références qui sont également capables de gérer la durée de vie, telles que les pointeurs intelligents. Une alternative pourrait être d'utiliser un gestionnaire. Notez que, lambda est une sorte de portée détachable; les captures lambda se comportent comme ayant une portée d'objet. Par conséquent, soyez prudent avec les captures lambda. Faites également attention à la façon dont le lambda lui-même est transmis - par copie ou par référence.
Quand passer par valeur
Pour les valeurs scalaires (primitives standard qui s'inscrivent dans un registre de machine et ont une valeur sémantique) pour lesquelles il n'y a pas besoin de communication par mutabilité (référence partagée), passez par valeur.
Pour les situations où l'appelé nécessite le clonage d'un objet ou d'un agrégat, passez par valeur, dans lequel la copie de l'appelé répond au besoin d'un objet cloné.
Quand passer par référence, etc.
pour toutes les autres situations, passez par des pointeurs, des références, des pointeurs intelligents, des poignées (voir: idiome poignée-corps), etc. Chaque fois que ce conseil est suivi, appliquez le principe de const-correctness comme d'habitude.
Les objets (agrégats, objets, tableaux, structures de données) dont l'empreinte mémoire est suffisamment importante doivent toujours être conçus pour faciliter le passage par référence, pour des raisons de performances. Ce conseil s'applique définitivement lorsqu'il s'agit de centaines d'octets ou plus. Ce conseil est limite quand il fait des dizaines d'octets.
Paradigmes inhabituels
Il existe des paradigmes de programmation à usage spécial qui sont lourds de copies par intention. Par exemple, traitement de chaînes, sérialisation, communication réseau, isolation, habillage de bibliothèques tierces, communication interprocessus en mémoire partagée, etc. Dans ces domaines d'application ou paradigmes de programmation, les données sont copiées de structures en structures, ou parfois reconditionnées dans tableaux d'octets.
Comment la spécification du langage affecte cette réponse, avant que l' optimisation ne soit considérée.
Sub-TL; DR La propagation d'une référence ne doit invoquer aucun code; le passage par const-reference satisfait ce critère. Cependant, toutes les autres langues satisfont sans effort à ce critère.
(Les programmeurs C ++ novices sont invités à ignorer entièrement cette section.)
(Le début de cette section est en partie inspiré par la réponse de gnasher729. Cependant, une conclusion différente est atteinte.)
C ++ autorise les constructeurs de copie définis par l'utilisateur et les opérateurs d'affectation.
(C'est (était) un choix audacieux qui est (était) à la fois étonnant et regrettable. Il s'agit certainement d'une divergence par rapport à la norme acceptable d'aujourd'hui dans la conception de la langue.)
Même si le programmeur C ++ n'en définit pas une, le compilateur C ++ doit générer ces méthodes en fonction des principes du langage, puis déterminer si du code supplémentaire doit être exécuté autrement que
memcpy
. Par exemple, unclass
/struct
qui contient unstd::vector
membre doit avoir un constructeur de copie et un opérateur d'affectation non trivial.Dans d'autres langages, les constructeurs de copie et le clonage d'objets sont déconseillés (sauf lorsque cela est absolument nécessaire et / ou significatif pour la sémantique de l'application), car les objets ont une sémantique de référence, par conception de langage. Ces langages auront généralement un mécanisme de récupération de place basé sur l'accessibilité plutôt que sur la propriété basée sur la portée ou le comptage de références.
Lorsqu'une référence ou un pointeur (y compris la référence const) est transmis en C ++ (ou C), le programmeur est assuré qu'aucun code spécial (fonctions définies par l'utilisateur ou générées par le compilateur) ne sera exécuté, autre que la propagation de la valeur d'adresse (référence ou pointeur). Il s'agit d'une clarté de comportement avec laquelle les programmeurs C ++ sont à l'aise.
Cependant, la toile de fond est que le langage C ++ est inutilement compliqué, de sorte que cette clarté de comportement est comme une oasis (un habitat qui peut survivre) quelque part autour d'une zone de retombées nucléaires.
Pour ajouter plus de bénédictions (ou insultes), C ++ introduit des références universelles (valeurs r) afin de faciliter les opérateurs de déplacement définis par l'utilisateur (constructeurs de mouvement et opérateurs d'affectation de mouvement) avec de bonnes performances. Cela profite à un cas d'utilisation très pertinent (le déplacement (transfert) d'objets d'une instance à une autre), en réduisant le besoin de copie et de clonage en profondeur. Cependant, dans d'autres langues, il est illogique de parler d'un tel déplacement d'objets.
(Section hors sujet) Une section dédiée à un article, "Vous voulez de la vitesse? Passez par valeur!" écrit vers 2009.
Cet article a été écrit en 2009 et explique la justification de la conception de la valeur r en C ++. Cet article présente un contre-argument valable à ma conclusion dans la section précédente. Cependant, l'exemple de code et la déclaration de performance de l'article ont longtemps été réfutés.
Sub-TL; DR La conception de la sémantique de valeur r en C ++ permet une sémantique côté utilisateur étonnamment élégante sur un
Sort
fonction, par exemple. Cet élégant est impossible à modéliser (imiter) dans d'autres langues.Une fonction de tri est appliquée à toute une structure de données. Comme mentionné ci-dessus, ce serait lent si beaucoup de copies sont impliquées. En tant qu'optimisation des performances (qui est pratiquement pertinente), une fonction de tri est conçue pour être destructrice dans plusieurs langages autres que C ++. Destructif signifie que la structure de données cible est modifiée pour atteindre l'objectif de tri.
En C ++, l'utilisateur peut choisir d'appeler l'une des deux implémentations: une destructrice avec de meilleures performances, ou une normale qui ne modifie pas l'entrée. (Le modèle est omis par souci de concision.)
Outre le tri, cette élégance est également utile dans la mise en œuvre d'un algorithme destructeur de recherche médiane dans un tableau (initialement non trié), par partitionnement récursif.
Cependant, notez que la plupart des langues appliqueraient une approche d'arbre de recherche binaire équilibrée au tri, au lieu d'appliquer un algorithme de tri destructif aux tableaux. Par conséquent, la pertinence pratique de cette technique n'est pas aussi élevée qu'il n'y paraît.
Comment l'optimisation du compilateur affecte cette réponse
Lorsque l'inlining (et également l'optimisation de l'ensemble du programme / l'optimisation du temps de liaison) est appliquée à plusieurs niveaux d'appels de fonction, le compilateur est capable de voir (parfois de manière exhaustive) le flux de données. Lorsque cela se produit, le compilateur peut appliquer de nombreuses optimisations, dont certaines peuvent éliminer la création d'objets entiers en mémoire. Généralement, lorsque cette situation s'applique, peu importe si les paramètres sont passés par valeur ou par const-référence, car le compilateur peut analyser de manière exhaustive.
Cependant, si la fonction de niveau inférieur appelle quelque chose qui est au-delà de l'analyse (par exemple quelque chose dans une bibliothèque différente en dehors de la compilation, ou un graphe d'appel qui est tout simplement trop compliqué), alors le compilateur doit optimiser de manière défensive.
Les objets plus grands qu'une valeur de registre d'ordinateur peuvent être copiés par des instructions explicites de chargement / stockage de mémoire ou par un appel à la
memcpy
fonction vénérable . Sur certaines plates-formes, le compilateur génère des instructions SIMD afin de se déplacer entre deux emplacements de mémoire, chaque instruction déplaçant des dizaines d'octets (16 ou 32).Discussion sur la question de la verbosité ou du désordre visuel
Les programmeurs C ++ sont habitués à cela, c'est-à-dire que tant qu'un programmeur ne déteste pas C ++, la surcharge d'écriture ou de lecture de const-reference dans le code source n'est pas horrible.
Les analyses coûts-avantages auraient pu être effectuées à plusieurs reprises auparavant. Je ne sais pas s'il y en a des scientifiques qui devraient être cités. Je suppose que la plupart des analyses seraient non scientifiques ou non reproductibles.
Voici ce que j'imagine (sans preuve ni références crédibles) ...
la source
Cela ne conseille pas aux programmeurs d'utiliser les techniques les plus lentes disponibles. Il s'agit de se concentrer sur la clarté lors de l'écriture de programmes. Souvent, la clarté et l'efficacité sont un compromis: si vous ne devez en choisir qu'une seule, choisissez la clarté. Mais si vous pouvez atteindre les deux facilement, il n'est pas nécessaire de paralyser la clarté (comme signaler que quelque chose est une constante) juste pour éviter l'efficacité.
la source
Le passage par (référence [const] [rvalue]) | (valeur) devrait concerner l'intention et les promesses faites par l'interface. Cela n'a rien à voir avec les performances.
Règle générale de Richy:
la source
Théoriquement, la réponse devrait être oui. Et, en fait, c'est oui parfois - en fait, passer par référence const au lieu de simplement passer une valeur peut être une pessimisation, même dans les cas où la valeur transmise est trop grande pour tenir dans un seul registre (ou la plupart des autres heuristiques que les gens essaient d'utiliser pour déterminer quand passer par valeur ou non). Il y a des années, David Abrahams a écrit un article intitulé "Vous voulez de la vitesse? Passez par valeur!" couvrant certains de ces cas. Ce n'est plus facile à trouver, mais si vous pouvez déterrer une copie, cela vaut la peine d'être lu (IMO).
Dans le cas spécifique du passage par référence const, cependant, je dirais que l'idiome est si bien établi que la situation est plus ou moins inversée: à moins que vous ne sachiez que le type sera
char
/short
/int
/long
, les gens s'attendent à le voir passer par const référence par défaut, il est donc préférable d'y aller à moins que vous n'ayez une raison assez spécifique de faire autrement.la source