J'étudie actuellement comment écrire du code C++ efficace et, en ce qui concerne les appels de fonctions, une question me vient à l'esprit. En comparant cette fonction pseudocode:
not-void function-name () {
do-something
return value;
}
int main () {
...
arg = function-name();
...
}
avec cette fonction pseudocode par ailleurs identique:
void function-name (not-void& arg) {
do-something
arg = value;
}
int main () {
...
function-name(arg);
...
}
Quelle version est la plus efficace et sous quel rapport (temps, mémoire, etc.)? Si cela dépend, alors quand le premier serait-il plus efficace et quand le plus efficace serait-il le second?
Edit: Pour le contexte, cette question se limite aux différences indépendantes de la plate-forme matérielle et, dans l’ensemble, aux logiciels. Existe-t-il une différence de performance indépendante de la machine?
Edit: Je ne vois pas en quoi c'est un doublon. L’autre question consiste à comparer le passage par référence (code précédent) au passage par valeur (ci-dessous):
not-void function-name (not-void arg)
Ce qui n’est pas la même chose que ma question. Mon objectif n'est pas de savoir quel est le meilleur moyen de passer un argument à une fonction. Mon objectif est de savoir quel est le meilleur moyen de transmettre out un résultat à une variable de l'extérieur.
Tout d'abord, tenez compte du fait que le retour d'un objet sera toujours plus lisible (et très similaire en performances) que de le faire passer par référence. Cela pourrait donc être plus intéressant pour votre projet de retourner l'objet et d'augmenter la lisibilité sans avoir d'importantes différences de performances. . Si vous voulez savoir comment avoir le coût le plus bas, le problème est ce que vous devez retourner:
Si vous devez renvoyer un objet simple ou de base, les performances seront similaires dans les deux cas.
Si l'objet est si volumineux et complexe, le renvoyer nécessiterait une copie. Il pourrait être plus lent que le paramètre référencé, mais je pense que cela nécessiterait moins de mémoire.
Quoi qu'il en soit, il faut penser que les compilateurs font beaucoup d'optimisations qui rendent les performances très similaires. Voir Copie de décision .
Eh bien, il faut comprendre que la compilation n’est pas une tâche facile. de nombreuses considérations sont prises en compte lorsque le compilateur compile votre code.
On ne peut pas simplement répondre à cette question car le standard C++ ne fournit pas le standard ABI (interface binaire abstraite). Par conséquent, chaque compilateur est autorisé à compiler le code à sa guise et vous pouvez obtenir des résultats différents dans chaque compilation.
Par exemple, sur certains projets, C++ est compilé en une extension gérée de Microsoft CLR (C++/CX). comme tout fait déjà référence à un objet sur le tas, je suppose qu'il n'y a pas de différence.
La réponse n'est pas plus simple pour les compilations non gérées. Plusieurs questions me viennent à l’esprit lorsque je pense à "Est-ce que XXX va courir plus vite que YYY?", par exemple:
std::array
) ou a-t-il un pointeur sur quelque chose sur le tas? (par exemple. std::vector
)?Si je donne un exemple concret, je suppose que sur MSVC++ et GCC, renvoyer std::vector
par valeur sera celui qui le passera par référence, en raison de l'optimisation de la valeur r, et sera un bit (de quelques nanosecondes) plus rapidement que le vecteur retourné par déplacement. cela peut être complètement différent sur Clang, par exemple.
finalement, le profilage est la seule vraie réponse ici.
Le renvoi de l'objet doit être utilisé dans la plupart des cas en raison d'une optimisation appelée copie de la décision .
Toutefois, selon l'utilisation prévue de votre fonction, il peut être préférable de transmettre l'objet par référence.
Regarder std::getline
_ par exemple, qui prend un std::string
par référence. Cette fonction est destinée à être utilisée comme condition de boucle et continue de remplir un std::string
jusqu’à EOF est atteint. Utiliser le même std::string
permet l’espace mémoire du std::string
à réutiliser à chaque itération de boucle, ce qui réduit considérablement le nombre d’allocations de mémoire à effectuer.
Certaines des réponses ont abordé ce sujet, mais je voudrais insister à la lumière de l'édition
Pour le contexte, cette question se limite aux différences indépendantes de la plate-forme matérielle et, dans l’ensemble, des logiciels. Existe-t-il une différence de performance indépendante de la machine?
Si telle est la limite de la question, la réponse est qu’il n’ya pas de réponse. La spécification c ++ ne stipule pas comment soit le retour d'un objet, soit un passage par référence est implémenté en termes de performances, seule la sémantique de ce qu'ils font tous les deux en termes de code.
Un compilateur est donc libre d’optimiser un code identique à l’autre en supposant que cela ne crée pas de différence perceptible pour le programmeur.
À la lumière de cela, je pense qu'il est préférable d'utiliser celui qui est le plus intuitif pour la situation. Si la fonction "renvoie" effectivement un objet à la suite d'une tâche ou d'une requête, renvoyez-le. Si la fonction effectue une opération sur un objet appartenant au code extérieur, transmettez-la par référence.
Vous ne pouvez pas généraliser les performances à ce sujet. Pour commencer, faites tout ce qui est intuitif et voyez dans quelle mesure votre système cible et son compilateur l’optimisent. Si, après le profilage, vous découvrez un problème, modifiez-le si vous en avez besoin.
Nous ne pouvons pas être 100% généraux, car différentes plates-formes ont différentes ABI, mais je pense que nous pouvons faire des déclarations assez générales qui s'appliqueront à la plupart des implémentations, mais avec l'avertissement que cela s'applique principalement à des fonctions qui ne sont pas en ligne.
Tout d'abord, considérons les types primitifs. À un niveau bas, un paramètre passe par référence est implémenté à l'aide d'un pointeur, tandis que les valeurs de retour primitives sont généralement transmises littéralement dans des registres. Donc, les valeurs de retour sont susceptibles de mieux fonctionner. Sur certaines architectures, il en va de même pour les petites structures. Copier une valeur suffisamment petite pour tenir dans un registre ou deux est très économique.
Considérons maintenant des valeurs plus grandes mais toujours simples (pas de constructeurs par défaut, constructeurs de copie, etc.). Généralement, les valeurs de retour les plus grandes sont gérées en transmettant à la fonction un pointeur vers l'emplacement où la valeur de retour doit être placée. Copier élision permet de fusionner la variable renvoyée par la fonction, le temporaire utilisé pour le retour et la variable de l'appelant dans laquelle le résultat est placé. Ainsi, les bases du passage seraient sensiblement les mêmes pour passe par référence et valeur de retour.
Globalement, pour les types primitifs, les valeurs de retour devraient être légèrement meilleures, tandis que pour les types plus grands mais toujours simples, elles devraient être identiques ou meilleures, à moins que votre compilateur ne soit très mauvais à la copie.
Pour les types qui utilisent des constructeurs par défaut, des constructeurs de copie, etc., les choses deviennent plus complexes. Si la fonction est appelée plusieurs fois, les valeurs renvoyées obligeront à reconstruire l'objet à chaque fois, tandis que les paramètres de référence peuvent permettre à la structure de données d'être réutilisée sans être reconstruite. D'autre part, les paramètres de référence forceront une construction (éventuellement inutile) avant l'appel de la fonction.
Cette fonction pseudocode:
not-void function-name () {
do-something
return value;
}
serait mieux utilisé lorsque la valeur renvoyée ne nécessite aucune modification supplémentaire. Le paramètre transmis n’est modifié que dans le function-name
. Il n'y a plus de références requises.
fonction pseudocode par ailleurs identique:
void function-name (not-void& arg) {
do-something
arg = value;
}
serait utile si nous avions une autre méthode modérant la valeur de la même variable, comme si nous devions conserver les modifications apportées à la variable par l'un ou l'autre des appels.
void another-function-name (not-void& arg) {
do-something
arg = value;
}
En termes de performances, les copies sont généralement plus chères, bien que la différence puisse être négligeable pour les petits objets. En outre, votre compilateur peut optimiser une copie de retour dans un déplacement, ce qui équivaut à transmettre une référence.
Je vous recommande de ne pas transmettre de références non -const
sauf si vous avez une bonne raison de le faire. Utilisez la valeur de retour (par exemple, les fonctions de la sorte tryGet()
).
Si vous le souhaitez, vous pouvez mesurer vous-même la différence, comme d'autres l'ont déjà dit. Exécutez le code de test plusieurs millions de fois pour les deux versions et constatez la différence.