web-dev-qa-db-fra.com

Passer par valeur plus rapidement que passer par référence

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;
}
44

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.

81
jliv902

Frais généraux avec passage par référence:

  • chaque accès nécessite une déréférence, c'est-à-dire qu'il y a une lecture de mémoire supplémentaire

Frais généraux avec passage par valeur:

  • la valeur doit être copiée sur la pile ou dans des registres

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.

17
green lantern

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.

11
Cosmic Bacon

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.

11
Kahler

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).

6
Spacemoose

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.

4
PureW

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.

4
Mark Ransom

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.

3