web-dev-qa-db-fra.com

Coût de performance du passage par valeur vs par référence ou par pointeur?

Considérons un objet foo (qui peut être un int, un double, un struct personnalisé, un class, peu importe). Ma compréhension est que passer foo par référence à une fonction (ou simplement passer un pointeur à foo) conduit à des performances plus élevées car nous évitons de faire une copie locale (qui pourrait être coûteuse si foo est grand).

Cependant, d'après la réponse ici il semble que les pointeurs sur un système 64 bits peuvent en pratique avoir une taille de 8 octets, indépendamment de ce qui est pointé. Sur mon système, un float fait 4 octets. Est-ce à dire que si foo est de type float, alors il est plus efficace de simplement passer foo par valeur plutôt que de lui donner un pointeur (en supposant qu'aucune autre contrainte ne rendrait l'utilisation de l'une plus efficace que l'autre à l'intérieur de la fonction)?

23
space_voyager

Cela dépend de ce que vous entendez par "coût" et des propriétés du système hôte (matériel, système d'exploitation) par rapport aux opérations.

Si votre mesure de coût est l'utilisation de la mémoire, le calcul du coût est évident - additionnez les tailles de tout ce qui est copié.

Si votre mesure est la vitesse d'exécution (ou "efficacité"), le jeu est différent. Le matériel (et les systèmes d'exploitation et le compilateur) ont tendance à être optimisés pour les performances des opérations de copie de choses de tailles particulières, grâce à des circuits dédiés (registres de machines et comment ils sont utilisés).

Il est courant, par exemple, qu'une machine ait une architecture (registres de machine, architecture de mémoire, etc.) qui crée un "point idéal" - la copie de variables d'une certaine taille est plus "efficace", mais une copie plus grande OR les petites variables le sont moins. Les variables plus grandes coûteront plus cher à copier, car il peut être nécessaire de faire plusieurs copies de petits morceaux. Les plus petites peuvent également coûter plus cher, car le compilateur doit copier la plus petite valeur dans une variable plus grande (ou registre), effectuez les opérations dessus, puis copiez la valeur.

Des exemples avec virgule flottante incluent certains supercalculateurs cray, qui prennent en charge nativement la virgule flottante double précision (aka double en C++), et toutes les opérations sur la précision simple (aka float en C++) sont émulées dans le logiciel. Certains processeurs x86 32 bits plus anciens fonctionnaient également en interne avec des entiers 32 bits, et les opérations sur des entiers 16 bits nécessitaient plus de cycles d'horloge en raison de la conversion vers/à partir de 32 bits (ce n'est pas le cas avec les 32 bits ou 64 bits plus modernes). processeurs x86 bits, car ils permettent de copier des entiers 16 bits vers/depuis des registres 32 bits et de les utiliser, avec moins de pénalités).

Il est un peu évident que copier une très grande structure en valeur sera moins efficace que de créer et de copier son adresse. Mais, en raison de facteurs comme ceux mentionnés ci-dessus, le point de croisement entre "mieux copier quelque chose de cette taille par valeur" et "mieux passer son adresse" est moins clair.

Les pointeurs et les références ont tendance à être implémentés de manière similaire (par exemple, le passage par référence peut être implémenté de la même manière que le passage d'un pointeur), mais cela n'est pas garanti.

La seule façon d'en être sûr est de le mesurer. Et réalisez que les mesures varient d'un système à l'autre.

19
Peter

Il y a une chose que personne n'a mentionnée.

Il existe une certaine optimisation GCC appelée IPA SRA, qui remplace automatiquement "passe par référence" par "passe par valeur": https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html = (-fipa-sra)

Ceci est très probablement fait pour les types scalaires (par exemple, int, double, etc.), qui n'ont pas de sémantique de copie non par défaut et peuvent tenir dans les registres cpu.

Cela fait

void(const int &f)

probablement aussi rapide (et optimisé en espace)

void(int f)

Donc, avec cette optimisation activée, l'utilisation de références pour les petits types devrait être aussi rapide que leur transmission par valeur.

D'un autre côté, passer (par exemple) std :: string par valeur n'a pas pu être optimisé à la vitesse de référence, car la sémantique de copie personnalisée est impliquée.

D'après ce que je comprends, utiliser pass by reference pour tout ne devrait jamais être plus lent que de choisir manuellement ce qu'il faut passer par valeur et ce qu'il faut passer par référence.

Ceci est extrêmement utile, en particulier pour les modèles:

template<class T>
void f(const T&)
{
    // Something
}

est toujours optimal

10
peku33

Vous devez tester n'importe quel scénario donné où les performances sont absolument critiques, mais soyez très prudent lorsque vous essayez de forcer le compilateur à générer du code d'une manière spécifique.

L'optimiseur du compilateur est autorisé à réécrire votre code de la manière qu'il choisit tant que le résultat final est le même, ce qui peut conduire à des optimisations très agréables.

Considérez que le passage d'un float par valeur nécessite de faire une copie du float, mais dans les bonnes conditions, le passage d'un float par référence pourrait permettre de stocker le float d'origine dans un registre à virgule flottante du CPU, et traiter ce registre comme le paramètre "référence" à la fonction. En revanche, si vous passez une copie, le compilateur doit trouver un endroit pour stocker la copie afin de conserver le contenu du registre, ou pire, il peut ne pas être en mesure d'utiliser un registre du tout en raison de la nécessité de préserver l'original (c'est particulièrement vrai dans les fonctions récursives!).

Cette différence est également importante si vous passez la référence à une fonction qui pourrait être insérée, où la référence peut réduire le coût de l'inline car le compilateur n'a pas à garantir qu'un paramètre copié ne peut pas modifier l'original.

Plus un langage vous permet de vous concentrer sur la description de ce que vous voulez faire plutôt que sur la façon dont vous le souhaitez, plus le compilateur est capable de trouver des façons créatives de faire le travail dur pour vous. En C++ en particulier, il est généralement préférable de ne pas se soucier des performances et de se concentrer plutôt sur la description de ce que vous voulez aussi clairement et simplement que possible. En essayant de décrire comment vous voulez que le travail soit fait, vous empêcherez tout aussi souvent le compilateur de faire son travail d'optimisation de votre code pour vous.

4
Matt Jordan

Est-ce à dire que si foo est de type float, alors il est plus efficace de simplement passer foo par valeur?

Passer un flotteur par valeur pourrait être plus efficace. Je m'attendrais à ce qu'il soit plus efficace - en partie à cause de ce que vous avez dit: Un flotteur est plus petit qu'un pointeur sur un système que vous décrivez. Mais en outre, lorsque vous copiez le pointeur, vous devez toujours déréférencer le pointeur pour obtenir la valeur dans la fonction. L'indirection ajoutée par le pointeur pourrait avoir un effet significatif sur les performances.

La différence d'efficacité pourrait être négligeable. En particulier, si la fonction peut être intégrée et que l'optimisation est activée, il n'y aura probablement pas de différence.

Vous pouvez savoir s'il y a un gain de performances en passant le flotteur par valeur dans votre cas en mesurant. Vous pouvez mesurer l'efficacité avec un outil de profilage.

Vous pouvez remplacer le pointeur par une référence et la réponse s'appliquera tout aussi bien.

Y a-t-il une sorte de surcharge dans l'utilisation d'une référence, la façon dont il y a quand un pointeur doit être déréférencé?

Oui. Il est probable qu'une référence ait exactement les mêmes caractéristiques de performance qu'un pointeur. S'il est possible d'écrire un programme sémantiquement équivalent en utilisant des références ou des pointeurs, les deux vont probablement générer un Assembly identique.


Si passer un petit objet par pointeur serait plus rapide que de le copier, alors ce serait sûrement vrai pour un objet de même taille, n'est-ce pas? Que diriez-vous d'un pointeur vers un pointeur, c'est à peu près la taille d'un pointeur, non? (C'est exactement la même taille.) Oh, mais les pointeurs sont aussi des objets. Donc, si passer un objet (tel qu'un pointeur) par pointeur est plus rapide que copier l'objet (le pointeur), passer un pointeur à un pointeur à un pointeur à un pointeur ... à un pointeur serait plus rapide que le progarm avec moins de pointeurs c'est encore plus rapide que celui qui n'utilisait pas de pointeurs ... Perhap's nous avons trouvé une source infinie d'efficacité ici :)

3
eerorika