J'ai créé un programme simple en c ++ pour comparer les performances entre deux approches - passer par valeur et passer par référence. En fait, passer par la valeur est meilleur que passer par la référence.
La conclusion devrait être que le passage par la valeur nécessite moins de cycles d'horloge (instructions)
Je serais vraiment heureux si quelqu'un pouvait expliquer en détail pourquoi passer par valeur nécessite moins de cycles d'horloge.
#include <iostream>
#include <stdlib.h>
#include <time.h>
using namespace std;
void function(int *ptr);
void function2(int val);
int main() {
int nmbr = 5;
clock_t start, stop;
start = clock();
for (long i = 0; i < 1000000000; i++) {
function(&nmbr);
//function2(nmbr);
}
stop = clock();
cout << "time: " << stop - start;
return 0;
}
/**
* pass by reference
*/
void function(int *ptr) {
*ptr *= 5;
}
/**
* pass by value
*/
void function2(int val) {
val *= 5;
}
Un bon moyen de savoir pourquoi il existe des différences est de vérifier le démontage. Voici les résultats que j'ai obtenus sur ma machine avec Visual Studio 2012.
Avec les indicateurs d'optimisation, les deux fonctions génèrent le même code:
009D1270 57 Push edi
009D1271 FF 15 D4 30 9D 00 call dword ptr ds:[9D30D4h]
009D1277 8B F8 mov edi,eax
009D1279 FF 15 D4 30 9D 00 call dword ptr ds:[9D30D4h]
009D127F 8B 0D 48 30 9D 00 mov ecx,dword ptr ds:[9D3048h]
009D1285 2B C7 sub eax,edi
009D1287 50 Push eax
009D1288 E8 A3 04 00 00 call std::operator<<<std::char_traits<char> > (09D1730h)
009D128D 8B C8 mov ecx,eax
009D128F FF 15 2C 30 9D 00 call dword ptr ds:[9D302Ch]
009D1295 33 C0 xor eax,eax
009D1297 5F pop edi
009D1298 C3 ret
Cela équivaut essentiellement à:
int main ()
{
clock_t start, stop ;
start = clock () ;
stop = clock () ;
cout << "time: " << stop - start ;
return 0 ;
}
Sans indicateurs d'optimisation, vous obtiendrez probablement des résultats différents.
fonction (pas d'optimisations):
00114890 55 Push ebp
00114891 8B EC mov ebp,esp
00114893 81 EC C0 00 00 00 sub esp,0C0h
00114899 53 Push ebx
0011489A 56 Push esi
0011489B 57 Push edi
0011489C 8D BD 40 FF FF FF lea edi,[ebp-0C0h]
001148A2 B9 30 00 00 00 mov ecx,30h
001148A7 B8 CC CC CC CC mov eax,0CCCCCCCCh
001148AC F3 AB rep stos dword ptr es:[edi]
001148AE 8B 45 08 mov eax,dword ptr [ptr]
001148B1 8B 08 mov ecx,dword ptr [eax]
001148B3 6B C9 05 imul ecx,ecx,5
001148B6 8B 55 08 mov edx,dword ptr [ptr]
001148B9 89 0A mov dword ptr [edx],ecx
001148BB 5F pop edi
001148BC 5E pop esi
001148BD 5B pop ebx
001148BE 8B E5 mov esp,ebp
001148C0 5D pop ebp
001148C1 C3 ret
function2 (pas d'optimisations)
00FF4850 55 Push ebp
00FF4851 8B EC mov ebp,esp
00FF4853 81 EC C0 00 00 00 sub esp,0C0h
00FF4859 53 Push ebx
00FF485A 56 Push esi
00FF485B 57 Push edi
00FF485C 8D BD 40 FF FF FF lea edi,[ebp-0C0h]
00FF4862 B9 30 00 00 00 mov ecx,30h
00FF4867 B8 CC CC CC CC mov eax,0CCCCCCCCh
00FF486C F3 AB rep stos dword ptr es:[edi]
00FF486E 8B 45 08 mov eax,dword ptr [val]
00FF4871 6B C0 05 imul eax,eax,5
00FF4874 89 45 08 mov dword ptr [val],eax
00FF4877 5F pop edi
00FF4878 5E pop esi
00FF4879 5B pop ebx
00FF487A 8B E5 mov esp,ebp
00FF487C 5D pop ebp
00FF487D C3 ret
Pourquoi le passage par valeur est-il plus rapide (dans le cas sans optimisation)?
Eh bien, function()
a deux opérations supplémentaires mov
. Jetons un coup d'œil à la première opération supplémentaire mov
:
001148AE 8B 45 08 mov eax,dword ptr [ptr]
001148B1 8B 08 mov ecx,dword ptr [eax]
001148B3 6B C9 05 imul ecx,ecx,5
Ici, nous déréférençons le pointeur. Dans function2 ()
, nous avons déjà la valeur, donc nous évitons cette étape. Nous déplaçons d'abord l'adresse du pointeur dans le registre eax. Ensuite, nous déplaçons la valeur du pointeur dans le registre ecx. Enfin, nous multiplions la valeur par cinq.
Regardons la deuxième opération supplémentaire mov
:
001148B3 6B C9 05 imul ecx,ecx,5
001148B6 8B 55 08 mov edx,dword ptr [ptr]
001148B9 89 0A mov dword ptr [edx],ecx
Maintenant, nous reculons. Nous venons de terminer la multiplication de la valeur par 5, et nous devons replacer la valeur dans l'adresse mémoire.
Comme function2 ()
n'a pas à gérer le référencement et le déréférencement d'un pointeur, il peut ignorer ces deux opérations supplémentaires mov
.
Frais généraux avec passage par référence:
Frais généraux avec passage par valeur:
Pour les petits objets, comme un entier, le passage par valeur sera plus rapide. Pour les objets plus gros (par exemple une grande structure), la copie créerait trop de surcharge, donc le passage par référence sera plus rapide.
Imaginez que vous entrez dans une fonction et que vous êtes censé entrer avec une valeur int. Le code de la fonction veut faire des choses avec cette valeur int.
Passer par valeur, c'est comme entrer dans la fonction et quand quelqu'un demande la valeur int foo, il suffit de la leur donner.
Passer par référence consiste à entrer dans la fonction avec l'adresse de la valeur int foo. Maintenant, chaque fois que quelqu'un a besoin de la valeur de foo, il doit aller le chercher. Tout le monde va se plaindre d'avoir à déréférencer foo tout le temps flippant. Je suis dans cette fonction depuis 2 millisecondes maintenant et j'ai dû chercher des milliers de fois! Pourquoi ne m'avez-vous pas donné la valeur en premier lieu? Pourquoi n'avez-vous pas passé par valeur?
Cette analogie m'a aidé à comprendre pourquoi passer par la valeur est souvent le choix le plus rapide.
Pour un certain raisonnement: dans les machines les plus populaires, un entier est de 32 bits et un pointeur de 32 ou 64 bits
Vous devez donc transmettre autant d'informations.
Pour multiplier un entier, vous devez:
Multipliez-le.
Pour multiplier un entier pointé par un pointeur, vous devez:
Déférer le pointeur. Multipliez-le.
J'espère que c'est assez clair :)
Passons maintenant à des choses plus spécifiques:
Comme cela a été souligné, votre fonction par valeur ne fait rien avec le résultat, mais celle par pointeur enregistre en fait le résultat en mémoire. Pourquoi tu es si injuste avec un pauvre pointeur? :( (je rigole)
Il est difficile de dire à quel point votre référence est valide, car les compilateurs sont livrés avec toutes sortes d'optimisations. (bien sûr, vous pouvez contrôler la liberté du compilateur, mais vous n'avez pas fourni d'informations à ce sujet)
Et enfin (et probablement le plus important), les pointeurs, valeurs ou références ne sont pas associés à une vitesse. Qui sait, vous pouvez trouver une machine plus rapide avec des pointeurs et avoir du mal avec les valeurs, ou l'inverse. D'accord, d'accord, il y a un modèle dans le matériel et nous faisons toutes ces hypothèses, la plus largement acceptée semble être:
Passez des objets simples par valeur et des objets plus complexes par référence (ou pointeur) (mais là encore, qu'est-ce qui est complexe? Qu'est-ce qui est simple? Cela change avec le temps comme suit le matériel)
Alors récemment, je sens que l'opinion standard devient: passer par la valeur et faire confiance au compilateur. Et c'est cool. Les compilateurs sont soutenus par des années de développement d'expertise et des utilisateurs en colère exigeant qu'il soit toujours meilleur.
Lorsque vous passez par valeur, vous dites au compilateur de faire une copie de l'entité que vous passez par valeur.
Lorsque vous passez par référence, vous dites au compilateur qu'il doit utiliser la mémoire réelle vers laquelle pointe la référence. Le compilateur ne sait pas si vous faites cela dans le but d'optimiser, ou parce que la valeur référencée peut être en train de changer dans un autre thread (par exemple). Il doit utiliser cette zone de mémoire.
Le passage par référence signifie que le processeur doit accéder à ce bloc de mémoire spécifique. Cela peut ou non être le processus le plus efficace, selon l'état des registres. Lorsque vous passez par référence, la mémoire de la pile peut être utilisée, ce qui augmente les chances d'accéder à la mémoire cache (beaucoup plus rapide).
Enfin, selon l'architecture de votre machine et le type que vous transmettez, une référence peut en fait être plus grande que la valeur que vous copiez. Copier un entier 32 bits implique de copier moins que de passer une référence sur une machine 64 bits.
Le passage par référence ne doit donc être effectué que lorsque vous avez besoin d'une référence (pour muter la valeur, ou parce que la valeur peut être modifiée ailleurs), ou lorsque la copie de l'objet référencé est plus coûteuse que de déréférencer la mémoire nécessaire.
Bien que ce dernier point ne soit pas trivial, une bonne règle d'or consiste à faire ce que Java fait: passer les types fondamentaux par valeur et les types complexes par référence (const).
Le passage par la valeur est souvent très rapide pour les petits types car la plupart d'entre eux sont plus petits que le pointeur sur les systèmes modernes (64 bits). Certaines optimisations peuvent également être effectuées lorsqu'elles sont transmises par valeur.
En règle générale, passez les types intégrés par valeur.
Dans ce cas, le compilateur s'est probablement rendu compte que le résultat de la multiplication n'était pas utilisé dans le cas du passage par valeur et l'a entièrement optimisé. Sans voir le code démonté, il est impossible d'être sûr.
L'exécution d'instructions de manipulation de mémoire 32 bits est assez souvent plus lente sur une plate-forme 64 bits native, car le processeur doit exécuter des instructions 64 bits malgré tout. Si le compilateur le fait correctement, les instructions 32 bits sont "appariées" dans le cache d'instructions, mais si une lecture 32 bits est exécutée avec une instruction 64 bits, 4 octets supplémentaires sont copiés en tant que remplissage puis supprimés. En bref, une valeur inférieure à la taille du pointeur ne signifie pas nécessairement qu'elle est plus rapide. Cela dépend de la situation et du compilateur, et ne doit absolument pas être pris en considération pour les performances, sauf pour les types composites où la valeur est nettement supérieure au pointeur d'une magnitude de 1, ou dans les cas où vous avez besoin des meilleures performances absolues pour une plate-forme particulière sans égard à la portabilité. Le choix entre le passage par référence ou par valeur ne doit dépendre que si vous souhaitez ou non que la procédure appelée puisse modifier l'objet passé. Si ce n'est qu'une lecture pour un type inférieur à 128 bits, passez par valeur, c'est plus sûr.