Différence Pointeur Référence.
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:aie:
Le test-code, j'ai réalisé une classe MonInt, contenant un int, et un accesseur pour le int:
Code:
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:
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:
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:
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:
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:
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:
1 2 3 4
| void fooPtr(MonInt* monInt)
{
std::cout << monInt->Geti();
} |
Passage par référence:
Code:
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:
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:
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:
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:
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:
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 :ccool:
Merci de votre lecture.