Je ne comprend pas. :koi:
Prenons le get() par exemple.
Sans unique_ptr j'ai:Avec unique_ptr j'ai:Code:pointer->DoSomething();
Il y a bien une opération en plus là, non?Code:pointer->get()->DoSomething();
Version imprimable
Je ne comprend pas. :koi:
Prenons le get() par exemple.
Sans unique_ptr j'ai:Avec unique_ptr j'ai:Code:pointer->DoSomething();
Il y a bien une opération en plus là, non?Code:pointer->get()->DoSomething();
non
correspond àCode:pointer->get()->DoSomething();
donc un pointer de pointeurCode:unique_ptr<t>* pointer;
Et tu n'es pas obligé de passer par get pour utiliser une fonction membreCode:pointer.get()->DoSomething();
Code:pointer->DoSomething();
euh... si tu passes par get() pour utiliser normalement unique_ptr ... !
pointeur->doSomething();
edit: HS.
J'ai l'impression que cela revient au même, car quand tu fais pointer->DoSomething(), l'operateur ->() de unique_ptr est appelé.
D'ailleurs, le test que je viens de faire semble le confirmer (l'asm généré est le même avec et sans get)
J'ai pris le code suivant:
J'ai compilé les 2 (en commentant l'un puis l'autre). Note: le cout c'est pour "faire quelque chose" avec mon pointeur sinon le compilateur ne prend pas la peine de le créer.Code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 struct Bar { Bar(int val = 0) : val(val) {} int val; }; main() { // 1. avec unique_ptr std::unique_ptr<Bar> p( new Bar(5) ); std::cout << p->val << std::endl; // 2. avec pointeur nu Bar * p = new Bar(5); std::cout << p->val << std::endl; }
Compilé avec vs2010, optimisation /O2 (maximize speed)
L'asm généré est (je montre seulement la ligne du cout), pour 1 (avec unique_ptr):
Et pour 2. (avec pointeur nu):Code:
1
2
3
4
5
6
7
8
9
10
11
12
13 ; 163 : std::cout << p->val << std::endl; 00049 a1 00 00 00 00 mov eax, DWORD PTR __imp_?endl@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@1@AAV21@@Z 0004e 8b 0e mov ecx, DWORD PTR [esi] 00050 50 push eax 00051 51 push ecx 00052 8b 0d 00 00 00 00 mov ecx, DWORD PTR __imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A 00058 ff 15 00 00 00 00 call DWORD PTR __imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@H@Z 0005e 8b c8 mov ecx, eax 00060 ff 15 00 00 00 00 call DWORD PTR __imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@P6AAAV01@AAV01@@Z@Z
On voit bien que, avec unique_ptr, on a un push en plus ( 00050 50 push eax).Code:
1
2
3
4
5
6
7
8
9
10
11
12
13 ; 167 : std::cout << p->val << std::endl; 00019 8b 0d 00 00 00 00 mov ecx, DWORD PTR __imp_?endl@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@1@AAV21@@Z 0001f 8b 10 mov edx, DWORD PTR [eax] 00021 51 push ecx 00022 8b 0d 00 00 00 00 mov ecx, DWORD PTR __imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A 00028 52 push edx 00029 ff 15 00 00 00 00 call DWORD PTR __imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@H@Z 0002f 8b c8 mov ecx, eax 00031 ff 15 00 00 00 00 call DWORD PTR __imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@P6AAAV01@AAV01@@Z@Z
Par contre, il me semble que là on reste au niveau des registres du processeur, donc pas d'accès ram. Mais tout de même, on a un push en plus.
Autre chose, ça n'a pas de sens de parler de "quelques cycles" dans l'absolue, sans donner de référentiel.
Si tu code un IHM qui passe 90% de son temps à attendre une action d'un utilisateur ou si tu fais des calculs qui prennent plusieurs milliers de cycles, perdre quelques cycles n'est pas du tout important.
Exemple sur un jeu. Tu as 60 FPS sur un proc 3 GHz, la perte d'un FPS correspond à 50 millions de cycles (calcul grossier). Ca laisse de la marge pour perdre quelques cycles avant que cela ait un impact visible
Bref, sauf si tu es dans un appli critique dans laquelle tu comptes les cycles un par un, l'impact sera ridicule par rapport à la sécurité apportée
(source : Programmation fonctionnelle en C++)Citation:
Envoyé par John Carmack
Pourtant Carmack est bien le genre à aller chipoter dans le code pour faire des hacks permettant de gagner quelques cycles...
Je suis tout à fait d'accord. Mais il existe tout de même des cas (extrêmement rares certes, mais ça existe, je suis sur un de ces cas en ce moment d'ailleurs), où le moindre cycle est important. Basiquement, j'ai une opération qui est appelée des milliards de fois (potentiellement des milliers de milliards de fois) et qui utilise plusieurs paramètres. La vitesse d'accès à ces paramètres est critique.
Tu lis mal. Tu as bien un move et un push dans les 2 cas (sur ecx pour unique_ptr et edx pour le pointeur nu, pas contre, le push de edx est inversé avec le move de ecx)
Ok, dans ce cas, tu es dans la situation (qui arrive dans 0,01% des cas) oùD'où l'intérêt d'avoir de la liberté pour ces 0,01% des cas et de fortement recommander (voir plus) les smarts pointeurs par défautCitation:
préfère privilégier ce qui facilite la sécurité, l'évolutivité, la maintenabilité, etc. sauf quand j'ai besoin explicitement de faire autrement.
Ha oui, tu as raison :oops: (du coup j'ai une grosse remise en question à faire).
Mais alors quoi, c'est le compilateur qui "efface" l'appel à l'opérateur ->() ?
Juste pour info, p.get()->value et p->value génèrent le même asm. p.get()->value:
Code:
1
2
3
4
5
6
7
8
9
10
11
12
13 ; 163 : std::cout << p.get()->val << std::endl; 00049 a1 00 00 00 00 mov eax, DWORD PTR __imp_?endl@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@1@AAV21@@Z 0004e 8b 0e mov ecx, DWORD PTR [esi] 00050 50 push eax 00051 51 push ecx 00052 8b 0d 00 00 00 00 mov ecx, DWORD PTR __imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A 00058 ff 15 00 00 00 00 call DWORD PTR __imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@H@Z 0005e 8b c8 mov ecx, eax 00060 ff 15 00 00 00 00 call DWORD PTR __imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@P6AAAV01@AAV01@@Z@Z
Autre remarque. Pour moi, partir sur des pointeurs nus par défaut au lieu des smart pointeurs, sans avoir testé dans l'algo que l'on implémente que cela à un impact visible, c'est une erreur. (prémature optimization, source des tous les maux)
Ah ok, je vois ce que tu veux dire. Oui, il y a bien un optimisation qui est faites par le compilateur, qui consiste à remplacer une fonction inliné directement par son code, pour éviter l'appelle d'une fonction.
En fait je crois que c'est un peu plus "sioux" que ça. Le fait d'inliner une fonction permet juste d'éviter un appel de fonction (donc le honnis changement de contexte). Ici, si on avait juste un "inlinage", le code avec unique_ptr serait (après "inlinage" mais avant génération):
Le "&**" venant de l'opérateur ->() de unique_ptr.Code:(&**p)->value;
Là il y a "inlinisation" + optimisation.
C'est là je pense ton erreur : L'inlining ne permet pas uniquement de supprimer un appel de fonction, mais aussi d'optimiser le code comme si tout était défini en un seul endroit. Parfois, le gain de perfs lié à ces potentialités d'optimisation supplémentaires est supérieur au gain de perfs lié à la suppression de l'appel de fonction.