Bonjour à tous,

Durant plusieurs échange sur ce forum concernant le faite de passer en référence, des objets ou des références, j'attire l'attention sur le faite que un accès via un pointeur est plus lent qu'un accès via une référence.
Je ne viens pas ouvrir ce poste pour être cartésien en disant que la référence est la solution ultime, mais le but est de prouvé que le pointeur n'est pas la solutions à prendre si nous avons le choix.
Vous trouverez-ci joint la code machine avec les informations suivantes:
- debug visual studio 2008.
- Release visual studio 2008 avec information de debug
- Optimisation complète ( /0x )

PS: Si quelqu'un sait comment observer le code machine des instanciations statiques en release ça m'arrangerais, je n'ai que le détail des instanciations dynamiques

Le test-code, j'ai réalisé une classe MonInt, contenant un int, et un accesseur pour le int:

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
struct MonInt
{
    int i_;
    MonInt():i_(){}
    MonInt(int i):i_(i){}
    MonInt(const MonInt& i)
        :i_(i.i_)
    {}
    int Geti(){ return i_; }
};
Mon test s'interroge sur plusieurs aspects:

1- La création
2- L'accès via déréférencement( * ou ->) ou via une référence (.)
3- Le passage par Pointeur ou référence


Je vais essayer d'être le plus pragmatique possible. Et de présenter les résultats de façon simple.


1- La création

La création est sans aucun doute plus rapide de façon statique, mais le but du test n'est pas ici, je le présente quand même pour ceux qui voudrait comprendre la réel différence.

Instanciation dynamique


Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
MonInt* monIntPtr = new MonInt(33);
 
---------------------------------------------
 
01261126  push        4    
01261128  call        operator new (126152Ah) 
0126112D  add         esp,4 
01261130  mov         dword ptr [ebp-2Ch],eax 
01261133  mov         dword ptr [ebp-4],0 
0126113A  cmp         dword ptr [ebp-2Ch],0 
0126113E  je          main+6Fh (126114Fh) 
01261140  push        21h  
01261142  mov         ecx,dword ptr [ebp-2Ch] 
01261145  call        MonInt::MonInt (1261005h) 
0126114A  mov         dword ptr [ebp-30h],eax 
0126114D  jmp         main+76h (1261156h) 
0126114F  mov         dword ptr [ebp-30h],0 
01261156  mov         eax,dword ptr [ebp-30h] 
01261159  mov         dword ptr [ebp-28h],eax 
0126115C  mov         dword ptr [ebp-4],0FFFFFFFFh 
01261163  mov         ecx,dword ptr [ebp-28h] 
01261166  mov         dword ptr [ebp-10h],ecx
On observe déjà une foule d'instructions. Pour l'opérateur new, pour la construction de la classe.

Instanciation statique

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
MonInt monIntRef(33);
 
---------------------------------------------
 
01261169  push        21h  
0126116B  lea         ecx,[ebp-18h] 
0126116E  call        MonInt::MonInt (1261005h)
Sans aucun doute, la création d'un variable statique est plus performante qu'une création dynamique.

Résultat(Debug):
- Dynamique : 18 instructions
- Statique : 3 instructions

Résultat(Release avec informations Debug):
- Dynamique : 9 instructions
- Statique : Aucun résultat


2- L'accès via déréférencement( * ou ->) ou via une référence (.)

C'est donc ici que l'on rentre dans le vive du sujet. Plusieurs fois j'ai lu que la différence pointeur et référence n'était que d'un point de vue esthétique ou conceptuel. Hors selon mes tests, il apparait qu'il y ai une instruction supplémentaire. Certains diront que les ordinateurs sont plus rapides aujourd'hui ça n'a aucun intérêt! Pour un logiciel de comptabilité pour la propriétaire d'un boulangerie sans aucun doute. Mais dans des milieux ou le moindre coup est prenable, non pas parce que on cherche la nano seconde sur une instruction, mais parce on cherche la nanoseconde * x présence dans le code ( huge code soit dit en passant ). Les jeux high-tech sur téléphone portable, les consoles de salon, voir les systèmes embarqués ( je ne connais pas ce milieu mais ça ce rapproche des consoles , voir c'est identique ).

Ce test sont réalisé avec :
- Release avec informations Debug sous Visual Studio 2008.
- Optimisation complète ( /0x )

accès via déréférencement( * ou ->)

Accès via l'opérateur *

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
std::cout << (*monIntPtr).i_;
 
---------------------------------------------
 
0108101B  mov         eax,dword ptr [esi] 
0108101D  mov         ecx,dword ptr [__imp_std::cout (108203Ch)] 
01081023  push        eax  
01081024  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (1082038h)]
Ici nous observons 4 instructions ( avec l'appel de cout , apparenté à l'appel d'une fonction operator<<() )

Accès via l'opérateur ->

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
std::cout << monIntPtr->i_;
 
---------------------------------------------
 
01081038  mov         ecx,dword ptr [esi] 
0108103A  push        ecx  
0108103B  mov         ecx,dword ptr [__imp_std::cout (108203Ch)] 
01081041  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (1082038h)]
Ici nous observons 4 instructions ( avec l'appel de cout , apparenté à l'appel d'une fonction operator<<() ). Identique à l'appel avec l'opérateur * à ceci prêt que deux instructions sont inversées. Si quelqu'un à une explication?

accès via une référence (.)

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
std::cout << monIntRef.i_;
 
---------------------------------------------
 
0108102A  mov         ecx,dword ptr [__imp_std::cout (108203Ch)] 
01081030  push        21h  
01081032  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (1082038h)]
Ici nous observons 3 instructions ( avec l'appel de cout , apparenté à l'appel d'une fonction operator<<() ). Donc une instruction de moins que l'accès via pointeur.

Résultat(Release avec informations Debug):
- Accès via déréférencement (* ou ->) : 4 instructions ( avec un appel de fonction operator<<() )
- Accès via une référence ( . ): 3 instructions ( avec un appel de fonction operator<<() )


3- Le passage par Pointeur ou référence


La ou ça deviens intéressant, c'est de ce demander si l'on dois passer par pointeur ou par référence. Hormis le faite que le pointeur peut ne pas être initialisé, par ça j'entends pointer sur l'adresse 0x00000000, et peut contrairement à la référence, référencer une nouvelle zone mémoire. La différence entre les deux reste subtile. Pour réaliser les tests, j'ai écris deux fonctions. En voici les définitions:

Passage par pointeur:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
void fooPtr(MonInt* monInt)
{
    std::cout << monInt->Geti();
}
Passage par référence:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
void fooRef(MonInt& monInt)
{
    std::cout << monInt.Geti();
}
J'ai volontairement mis les std::cout pour remettre en avant cette différence.

En considérant les variables créées comme suit:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
MonInt* monIntPtr = new MonInt(33);
MonInt monIntRef(33);
Voici les résultats aux l'appel des fonctions:

Passage par pointeur:

Passage d'un pointeur:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
fooPtr(monIntPtr);
 
---------------------------------------------
 
00E51047  mov         edx,dword ptr [esi] 
00E51049  mov         ecx,dword ptr [__imp_std::cout (0E5203Ch)] 
00E5104F  push        edx  
00E51050  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0E52038h)]
La fonction prenant en paramètre un pointeur, lui passant un pointeur. Ne consomme rien.

Passage d'une référence:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
fooPtr(&monIntRef);
 
---------------------------------------------
 
00E51056  mov         ecx,dword ptr [__imp_std::cout (0E5203Ch)] 
00E5105C  push        21h  
00E5105E  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0E52038h)]
La fonction prenant en paramètre un pointeur, lui passant un référence sur un objet de la pile. Ne consomme rien.

Passage par référence:

Passage d'un objet sur le tas via déréférencement:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
fooRef(*monIntPtr);
 
 ---------------------------------------------
 
00E51064  mov         eax,dword ptr [esi] 
00E51066  mov         ecx,dword ptr [__imp_std::cout (0E5203Ch)] 
00E5106C  push        eax  
00E5106D  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0E52038h)]
La fonction prenant en paramètre une référence, lui passant un déréférencement d'un objet sur le tas. Ne consomme rien.

Passage d'un objet sur la pile:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
fooRef(monIntRef);
 
  ---------------------------------------------
 
00E51073  mov         ecx,dword ptr [__imp_std::cout (0E5203Ch)] 
00E51079  push        21h  
00E5107B  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0E52038h)]
La fonction prenant en paramètre une référence, lui passant un un objet sur le pile. Ne consomme rien.

Résultat(Release avec informations Debug):
- Passage par pointeur : 0 instructions
- Passage par référence : 0 instructions

Après avoir observé ces résultats, nous sommes en mesure de se demander qu'elle est le but alors d'un test pareil? Sa n'a pas de sens!
Et pourtant si, le but est de comprendre que ce n'est pas le type de passage qui est important mais ce qu'il en découle. Nous avons observé que l'accès à des éléments via un pointeur (dynamique/tas) consomme une instruction supplémentaire que l'accès via une référence (statique/pile). Chaque consultation viendra ajouter une instruction. Et avec une hiérarchie plus profonde dans les classes, avec les boucles etc... Notre simple passage par pointeur viens de rajouter plusieurs centaines d'instructions! Des instructions qui nous aurais été utiles pour faire autres choses.

Je répète encore une fois que je ne dénigre pas le pointeur, je met en avant ce qu'il coûte de part son utilisation excessive et inutile.

Si il y a des critiques, des avis, des commentaires, des ajouts, ou des reformulations de mes propos, je suis là pour apprendre aussi

Merci de votre lecture.