J'ai appris le C ++ et dois souvent renvoyer des objets volumineux à partir de fonctions créées dans la fonction. Je sais qu'il y a passe par référence, renvoyer un pointeur et des solutions de type référence, mais j'ai aussi lu que les compilateurs C ++ (et la norme C ++) permettent l'optimisation des valeurs de retour, ce qui évite de copier ces gros objets en mémoire. économiser le temps et la mémoire de tout cela.
Maintenant, j'estime que la syntaxe est beaucoup plus claire lorsque l'objet est explicitement renvoyé par valeur, et que le compilateur emploiera généralement le RVO et rendra le processus plus efficace. Est-ce une mauvaise pratique de s'appuyer sur cette optimisation? Cela rend le code plus clair et plus lisible par l'utilisateur, ce qui est extrêmement important, mais devrais-je me garder de supposer que le compilateur saisira l'occasion RVO?
S'agit-il d'une micro-optimisation ou d'une chose à garder à l'esprit lors de la conception de mon code?
la source
Réponses:
Employer le principe de moindre surprise .
Est-ce vous et seulement jamais qui utiliserez ce code, et êtes-vous sûr que vous ne serez pas surpris de ce que vous faites dans 3 ans?
Alors vas-y.
Dans tous les autres cas, utilisez la méthode standard. sinon, vous et vos collègues allez rencontrer des difficultés pour trouver des bogues.
Par exemple, mon collègue se plaignait que mon code causait des erreurs. Il s'est avéré qu'il avait désactivé l'évaluation booléenne de court-circuit dans les paramètres de son compilateur. Je l'ai presque giflé.
la source
Pour ce cas particulier, il faut absolument revenir par valeur.
RVO et NRVO sont des optimisations bien connues et robustes qui devraient vraiment être faites par tout compilateur décent, même en mode C ++ 03.
La sémantique de déplacement garantit que les objets sont déplacés des fonctions si (N) RVO n'a pas eu lieu. Ce n'est utile que si votre objet utilise des données dynamiques en interne (comme le
std::vector
fait), mais cela devrait vraiment être le cas si elles sont aussi volumineuses: déborder de la pile est un risque pour les gros objets automatiques.C ++ 17 applique RVO. Alors ne vous inquiétez pas, il ne disparaîtra pas sur vous et ne finira par s'établir complètement que lorsque les compilateurs seront à jour.
Et au final, forcer une allocation dynamique supplémentaire à renvoyer un pointeur, ou forcer votre type de résultat à être constructible par défaut afin que vous puissiez le passer comme paramètre de sortie sont à la fois des solutions laides et non idiomatiques à un problème que vous ne rencontrerez probablement jamais. avoir.
Il suffit d’écrire du code qui a du sens et de remercier les rédacteurs du compilateur d’optimiser correctement le code qui a du sens.
la source
Ce n’est pas une micro-optimisation peu connue, mignonne, dont vous parlez dans un petit blog, qui fait l’objet d’un trafic, puis vous vous sentez malin et supérieur.
Après C ++ 11, RVO est la méthode standard pour écrire ce code de code. Il est courant, attendu, enseigné, mentionné dans les discussions, mentionné dans les blogs, mentionné dans la norme, sera signalé comme un bug du compilateur s'il n'est pas implémenté. En C ++ 17, le langage va encore plus loin et impose la résolution de copie dans certains scénarios.
Vous devez absolument compter sur cette optimisation.
En plus de cela, le retour par valeur conduit simplement à un code extrêmement facile à lire et à gérer qu'un code qui retourne par référence. La sémantique de valeur est une chose puissante, qui pourrait conduire à plus d'opportunités d'optimisation.
la source
L'exactitude du code que vous écrivez ne doit jamais dépendre d'une optimisation. Il devrait générer le résultat correct une fois exécuté sur la "machine virtuelle" C ++ utilisée dans la spécification.
Cependant, ce dont vous parlez est davantage une question d’efficacité. Votre code fonctionne mieux s'il est optimisé avec un compilateur optimisant RVO. C'est bien, pour toutes les raisons mentionnées dans les autres réponses.
Toutefois, si vous avez besoin de cette optimisation (par exemple, si le constructeur de la copie entraîne l’échec de votre code), vous êtes maintenant à la merci du compilateur.
Je pense que le meilleur exemple de cela dans ma propre pratique est l'optimisation des appels de queue:
C'est un exemple stupide, mais il montre un appel final, dans lequel une fonction est appelée récursivement juste à la fin d'une fonction. La machine virtuelle C ++ montrera que ce code fonctionne correctement, bien que je puisse créer un peu de confusion sur les raisons pour lesquelles je me suis ennuyé d'écrire une telle routine d'addition en premier lieu. Cependant, dans les implémentations pratiques de C ++, nous avons une pile et son espace est limité. Si elle est effectuée de manière pédagogique, cette fonction devra au moins
b + 1
insérer des cadres de pile dans la pile lors de son addition. Si je veux calculersillyAdd(5, 7)
, ce n'est pas grave. Si je veux calculersillyAdd(0, 1000000000)
, je pourrais avoir vraiment du mal à provoquer un StackOverflow (et non le bon genre ).Cependant, nous pouvons voir que lorsque nous atteignons la dernière ligne de retour, nous en avons vraiment fini avec tout dans le cadre de pile actuel. Nous n'avons pas vraiment besoin de le garder. L'optimisation des appels en queue vous permet de "réutiliser" le cadre de pile existant pour la fonction suivante. De cette façon, nous n’avons besoin que d’un seul cadre de pile, plutôt que
b+1
. (Nous devons encore faire toutes ces additions et soustractions idiotes, mais elles ne prennent pas plus de place.) En réalité, l'optimisation convertit le code en:Dans certaines langues, l’optimisation de l’appel final est explicitement requise par la spécification. C ++ n'en fait pas partie. Je ne peux pas compter sur les compilateurs C ++ pour reconnaître cette opportunité d'optimisation des appels en aval, sauf si j'y vais au cas par cas. Avec ma version de Visual Studio, la version finale optimise les appels en aval, contrairement à la version de débogage (de par sa conception).
Ainsi , il serait mauvais pour moi dépends d'être en mesure de calculer
sillyAdd(0, 1000000000)
.la source
#ifdef
blocs et propose une solution de contournement conforme aux normes.b = b + 1
?En pratique, les programmes C ++ attendent quelques optimisations du compilateur.
Recherchez notamment dans les en-têtes standard de vos implémentations de conteneurs standard . Avec GCC , vous pouvez demander le formulaire prétraité (
g++ -C -E
) et la représentation interne GIMPLE (g++ -fdump-tree-gimple
ou Gimple SSA avec-fdump-tree-ssa
) de la plupart des fichiers source (unités de traduction techniques) à l'aide de conteneurs. Vous serez surpris par la quantité d'optimisation qui est faite (avecg++ -O2
). Ainsi, les implémenteurs de conteneurs s'appuient sur les optimisations (et la plupart du temps, l'implémenteur d'une bibliothèque standard C ++ sait quelle optimisation se produirait et écrivait l'implémentation de conteneur en gardant cela à l'esprit; parfois, il écrivait également le passage d'optimisation dans le compilateur. traiter les fonctionnalités requises par la bibliothèque standard C ++).En pratique, ce sont les optimisations du compilateur qui rendent C ++ et ses conteneurs standard suffisamment efficaces. Vous pouvez donc compter sur eux.
Et de même pour l'affaire RVO mentionnée dans votre question.
La norme C ++ a été co-conçue (notamment en expérimentant des optimisations suffisamment bonnes tout en proposant de nouvelles fonctionnalités) pour bien fonctionner avec les optimisations possibles.
Par exemple, considérons le programme ci-dessous:
compilez-le avec
g++ -O3 -fverbose-asm -S
. Vous découvrirez que la fonction générée n'exécute aucuneCALL
instruction machine. Ainsi, la plupart des étapes C ++ (construction d’une fermeture lambda, application répétée, obtention dubegin
et desend
itérateurs, etc.) ont été optimisées. Le code machine ne contient qu'une boucle (qui n'apparaît pas explicitement dans le code source). Sans ces optimisations, C ++ 11 ne réussira pas.addenda
(ajouté le 31 décembre er 2017)
Voir CppCon 2017: Matt Godbolt «Qu'est-ce que mon compilateur a fait pour moi récemment? Déverrouiller le couvercle du compilateur » .
la source
Chaque fois que vous utilisez un compilateur, il est entendu qu’il produira pour vous un code machine ou octet. Cela ne garantit en rien ce que ce code généré est, si ce n'est qu'il implémentera le code source en fonction de la spécification du langage. Notez que cette garantie est la même quel que soit le niveau d'optimisation utilisé et qu'en règle générale, il n'y a donc aucune raison de considérer une sortie comme plus «juste» que l'autre.
De plus, dans les cas, comme RVO, où cela est spécifié dans la langue, il semblerait inutile de faire tout votre possible pour éviter de l’utiliser, en particulier si cela simplifie le code source.
On s’efforce beaucoup de faire en sorte que les compilateurs produisent des résultats efficaces, et l’intention est clairement d’utiliser ces capacités.
Il peut y avoir des raisons pour utiliser du code non optimisé (pour le débogage, par exemple), mais le cas mentionné dans cette question ne semble pas en être un (et si votre code échoue uniquement lorsqu'il est optimisé, il ne s'agit pas d'une conséquence de l’appareil sur lequel vous l’utilisez, alors il ya un bogue quelque part et il est peu probable qu’il soit dans le compilateur.)
la source
Je pense que d'autres ont bien abordé l'angle spécifique concernant C ++ et RVO. Voici une réponse plus générale:
Pour ce qui est de l'exactitude, vous ne devriez pas vous fier aux optimisations du compilateur, ni au comportement spécifique du compilateur en général. Heureusement, vous ne semblez pas le faire.
Pour ce qui est des performances, vous devez vous fier au comportement spécifique du compilateur en général et à ses optimisations en particulier. Un compilateur conforme aux normes est libre de compiler votre code comme bon lui semble, à condition que le code compilé se comporte conformément à la spécification du langage. Et je ne suis au courant d'aucune spécification pour un langage grand public spécifiant la rapidité d'exécution de chaque opération.
la source
Les optimisations du compilateur ne doivent affecter que les performances, pas les résultats. Compter sur les optimisations du compilateur pour répondre à des exigences non fonctionnelles est non seulement raisonnable, mais souvent aussi la raison pour laquelle un compilateur est sélectionné.
Les indicateurs qui déterminent la manière dont des opérations particulières sont effectuées (conditions d'index ou de dépassement de capacité, par exemple), sont souvent regroupés avec les optimisations du compilateur, mais ne devraient pas l'être. Ils affectent explicitement les résultats des calculs.
Si une optimisation du compilateur entraîne des résultats différents, il s'agit d'un bogue - un bogue dans le compilateur. S'appuyant sur un bug dans le compilateur, est à long terme une erreur - que se passe-t-il quand il est corrigé?
L'utilisation d'indicateurs de compilation qui modifient le fonctionnement des calculs doit être bien documentée, mais utilisée au besoin.
la source
x*y>z
donne arbitrairement 0 ou 1 en cas de dépassement, à condition qu'il n'ait aucun autre effet secondaire , obliger le programmeur à empêcher les débordements à tout prix ou à forcer le compilateur à évaluer l'expression d'une manière particulière. Inutile altérer les optimisations vs dire que ...x*y
promouvait ses opérandes selon un type plus long et arbitraire (permettant ainsi des formes de levage et de réduction de la résistance qui modifieraient le comportement de certains cas de dépassement de capacité). Cependant, de nombreux compilateurs exigent que les programmeurs empêchent le débordement à tout prix ou obligent les compilateurs à tronquer toutes les valeurs intermédiaires en cas de débordement.Non.
C'est ce que je fais tout le temps. Si j'ai besoin d'accéder à un bloc arbitraire de 16 bits en mémoire, je le fais
... et comptez sur le compilateur qui fera tout ce qui est en son pouvoir pour optimiser ce morceau de code. Le code fonctionne sur ARM, i386, AMD64 et pratiquement sur toutes les architectures existantes. En théorie, un compilateur non optimiseur peut en réalité appeler
memcpy
, ce qui entraîne des performances totalement mauvaises, mais ce n’est pas un problème pour moi, car j’utilise les optimisations du compilateur.Considérez l'alternative:
Ce code alternatif ne fonctionne pas sur les machines qui nécessitent un alignement correct, si
get_pointer()
un pointeur non aligné est renvoyé. En outre, il peut y avoir des problèmes d'aliasing dans l'alternative.La différence entre -O2 et -O0 lors de l'utilisation de l'
memcpy
astuce est grande: performances de somme de contrôle IP de 3,2 Gbps contre performances de somme de contrôle IP de 67 Gbps. Plus d'un ordre de grandeur!Parfois, vous devrez peut-être aider le compilateur. Ainsi, par exemple, au lieu de compter sur le compilateur pour dérouler les boucles, vous pouvez le faire vous-même. Soit en implémentant le célèbre appareil de Duff , soit de manière plus propre.
L’inconvénient de s’appuyer sur les optimisations du compilateur est que, si vous exécutez gdb pour déboguer votre code, vous constaterez peut-être que beaucoup de choses ont été optimisées. Donc, vous devrez peut-être recompiler avec -O0, ce qui signifie que les performances seront totalement nul lors du débogage. Je pense que c'est un inconvénient qui vaut la peine d'être pris compte tenu des avantages de l'optimisation des compilateurs.
Quoi que vous fassiez, assurez-vous que votre comportement n’est pas un comportement indéfini. Accéder à un bloc de mémoire aléatoire en tant qu'entier 16 bits est un comportement indéfini en raison de problèmes d'alias et d'alignement.
la source
Toutes les tentatives visant à obtenir un code efficace écrit autrement qu'en assemblage reposent énormément sur les optimisations du compilateur, à commencer par l'allocation de registre la plus élémentaire comme efficace pour éviter les débordements superflus de pile et au moins une sélection d'instructions raisonnablement bonne, sinon excellente. Sinon, nous reviendrions aux années 80, où nous devions mettre des
register
indices partout et utiliser le nombre minimum de variables dans une fonction pour aider les compilateurs C archaïques, ou même plus tôt, à une époque où l'goto
optimisation des branches était utile.Si nous n'avions pas le sentiment de pouvoir compter sur la capacité de notre optimiseur à optimiser notre code, nous serions toujours en train de coder des chemins d'exécution critiques pour la performance dans l'assembly.
C’est vraiment une question de fiabilité. Selon vous, l’optimisation peut être mieux réglée en établissant un profil et en examinant les capacités des compilateurs que vous possédez, voire en la désassemblant s’il ya un point chaud dans lequel vous ne pouvez pas savoir où le compilateur semble ont échoué à faire une optimisation évidente.
RVO est quelque chose qui existe depuis des lustres et, à tout le moins en excluant les cas très complexes, les compilateurs s’appliquent bien depuis des lustres. Ce n'est certainement pas la peine de travailler sur un problème qui n'existe pas.
Err sur le côté de s'appuyer sur l'optimiseur, ne pas le craindre
Au contraire, je dirais qu’il ne faut pas trop miser sur l’optimisation du compilateur, et cette suggestion émane de quelqu'un qui travaille dans des domaines très critiques en termes de performances, où efficacité, maintenabilité et qualité perçue par les clients sont primordiales. tous un flou géant. Je préférerais que vous dépendiez trop de votre optimiseur avec confiance et que vous trouviez des cas obscurs où vous vous reposiez trop, plutôt que trop peu et que vous codiez tout le temps à partir de peurs superstitieuses pour le reste de votre vie. Cela vous permettra au moins de rechercher un profileur et d’enquêter correctement si les choses ne s’exécutent pas aussi vite qu’elles le devraient et d’acquérir des connaissances précieuses, et non des superstitions, en cours de route.
Vous vous débrouillez bien pour vous appuyer sur l'optimiseur. Continuez. Ne devenez pas comme ce type qui commence à demander explicitement à toutes les fonctions appelées dans une boucle d'être insérées dans une boucle avant même de vous profiler pour ne pas craindre les faiblesses de l'optimiseur.
Profilage
Le profilage est vraiment le rond-point mais la réponse ultime à votre question. Le problème que les débutants désirant écrire du code efficace ont souvent du mal à résoudre n’est pas ce qu’il faut optimiser, c’est ce qu’il ne faut pas optimiser car ils développent toutes sortes de intuitions erronées concernant des inefficacités qui, tout en étant humainement intuitives, sont fausses en calcul. L’expérience de développement avec un profileur commencera vraiment à vous donner une bonne idée des capacités d’optimisation de vos compilateurs sur lesquelles vous pouvez compter en toute confiance, mais également des capacités (ainsi que des limitations) de votre matériel. Il est sans doute encore plus utile de profiler pour apprendre ce qui ne valait pas la peine d'être optimisé que d'apprendre ce qui était.
la source
Les logiciels peuvent être écrits en C ++ sur des plateformes très différentes et à des fins très diverses.
Cela dépend complètement de l'objectif du logiciel. Devrait-il être facile à maintenir, développer, patcher, refactor et.c. ou bien d’autres facteurs plus importants, tels que les performances, le coût ou la compatibilité avec un matériel spécifique ou le temps qu’il prend pour se développer.
la source
Je pense que la réponse ennuyeuse à cette question est: "ça dépend".
Est-ce une mauvaise pratique d’écrire du code qui repose sur une optimisation du compilateur susceptible d’être désactivée et où la vulnérabilité n’est pas documentée et où le code en question n’est pas testé par unité de sorte que si cela se cassait, vous le sauriez ? Probablement.
Est-ce une mauvaise pratique d’écrire du code reposant sur une optimisation du compilateur peu susceptible d’être désactivée , documentée et testée par unité ? Peut être pas.
la source
À moins qu'il n'y ait plus que vous ne nous dites pas, c'est une mauvaise pratique, mais pas pour la raison que vous suggérez.
Peut-être contrairement aux autres langages que vous avez utilisés auparavant, le renvoi de la valeur d'un objet en C ++ génère une copie de celui-ci. Si vous modifiez ensuite l'objet, vous modifiez un autre objet . C'est-à-dire que si j'ai
Obj a; a.x=1;
etObj b = a;
, alors je faisb.x += 2; b.f();
, alorsa.x
toujours égal à 1, pas 3.Donc non, utiliser un objet comme valeur plutôt que comme référence ou pointeur ne fournit pas les mêmes fonctionnalités et vous pourriez vous retrouver avec des bogues dans votre logiciel.
Peut-être que vous le savez et que cela n’affecte pas votre cas d’utilisation spécifique. Toutefois, selon le libellé de votre question, il semble que vous ne soyez peut-être pas au courant de la distinction; des termes tels que "créer un objet dans la fonction".
"créer un objet dans la fonction" sonne comme
new Obj;
"retourne l'objet par valeur" sonneObj a; return a;
Obj a;
etObj* a = new Obj;
sont des choses très très différentes; le premier peut entraîner une corruption de la mémoire s'il n'est pas utilisé et compris correctement, et le dernier peut entraîner des fuites de mémoire s'il n'est pas utilisé et compris correctement.la source
return
instruction, condition requise pour RVO. En outre, vous abordez ensuite les mots clésnew
et les pointeurs, ce qui n’est pas l’objet de RVO. Je crois que vous ne comprenez pas la question, ou RVO, ou peut-être les deux.Pieter B a tout à fait raison de recommander le moins d'étonnement.
Pour répondre à votre question spécifique, ce que cela (le plus probable) signifie en C ++, c'est que vous devez renvoyer a
std::unique_ptr
à l'objet construit.La raison en est que cela est plus clair pour un développeur C ++ en ce qui concerne ce qui se passe.
Bien que votre approche fonctionne probablement, vous signalez effectivement que l'objet est un type de valeur faible alors qu'en réalité il ne l'est pas. En plus de cela, vous éliminez toute possibilité d'abstraction d'interface. Cela peut convenir à vos objectifs actuels, mais est souvent très utile lorsque vous utilisez des matrices.
J'apprécie que si vous venez d'autres langues, tous les sigils peuvent être déroutants au début. Mais veillez à ne pas supposer que, en ne les utilisant pas, vous rendez votre code plus clair. En pratique, l'inverse est susceptible d'être vrai.
la source
std::make_unique
, passtd::unique_ptr
directement. Deuxièmement, RVO n’est pas une optimisation ésotérique, propre au fournisseur: elle est intégrée au standard. Même à l'époque où ce n'était pas le cas, c'était un comportement largement soutenu et attendu. Il ne sert à rien de renvoyer un pointstd::unique_ptr
lorsqu'un pointeur n'est pas nécessaire.