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:
Mon test s'interroge sur plusieurs aspects:
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_; } };
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
On observe déjà une foule d'instructions. Pour l'opérateur new, pour la construction de la classe.
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
Instanciation statique
Sans aucun doute, la création d'un variable statique est plus performante qu'une création dynamique.
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)
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 *
Ici nous observons 4 instructions ( avec l'appel de cout , apparenté à l'appel d'une fonction operator<<() )
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)]
Accès via l'opérateur ->
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?
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)]
accès via une référence (.)
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.
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)]
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:
Passage par référence:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4 void fooPtr(MonInt* monInt) { std::cout << monInt->Geti(); }
J'ai volontairement mis les std::cout pour remettre en avant cette différence.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4 void fooRef(MonInt& monInt) { std::cout << monInt.Geti(); }
En considérant les variables créées comme suit:
Voici les résultats aux l'appel des fonctions:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2 MonInt* monIntPtr = new MonInt(33); MonInt monIntRef(33);
Passage par pointeur:
Passage d'un pointeur:
La fonction prenant en paramètre un pointeur, lui passant un pointeur. Ne consomme rien.
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)]
Passage d'une référence:
La fonction prenant en paramètre un pointeur, lui passant un référence sur un objet de la pile. Ne consomme rien.
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)]
Passage par référence:
Passage d'un objet sur le tas via déréférencement:
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.
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)]
Passage d'un objet sur la pile:
La fonction prenant en paramètre une référence, lui passant un un objet sur le pile. Ne consomme rien.
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)]
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.
Partager