Developpez.com

Plus de 14 000 cours et tutoriels en informatique professionnelle à consulter, à télécharger ou à visionner en vidéo.

Faut-il bannir les boucles "for" du C++
Au profit des algorithmes de la bibliothèque standard ?

Le , par gbdivers, Inactif
La boucle for face aux algorithmes de la bibliothèque standard

J'ai donné un jour un exercice à l'un de mes stagiaires : modifier tout le code d'un projet qu'il avait écrit pour remplacer toutes les boucles for par des algorithmes de la bibliothèque standard.

Au-delà de ma simple tendance naturelle à torturer les stagiaires, je trouvais cet exercice intéressant pour plusieurs raisons.
La première est pédagogique, pour habituer mon stagiaire à apprendre et à utiliser les outils existants de la bibliothèque standard plutôt que repartir systématiquement de zéro.
La seconde raison est une question d'expressivité. Lorsque l'on rencontre un for dans le code, on ne peut pas savoir ce que va faire ce code. Il est nécessaire de lire le contenu du bloc et cela peut devenir très complexe, surtout si on appelle beaucoup de fonctions et que l'on manipule de nombreux objets dans ce bloc.
La dernière raison est plus conceptuelle. Lorsque l'on conçoit son algorithme en terme de boucle for, on réfléchit en fait en terme d'implémentation. Penser en termes d'algorithmes de la bibliothèque standard, c'est penser en termes de concepts que l'on manipule. On obtiendra alors plus facilement du code propre, maintenable et évolutif.

Que pensez-vous de cette attitude de supprimer, sauf choix conscient et justifié, les boucles for du code au profit de la STL ?
Voyez-vous d'autres justifications ou contre-arguments pour l'utilisation systématique des algorithmes de la STL ?
D'un point de vue pédagogique, que pensez-vous de cet exercice ?
Dans un cadre professionnel, pensez-vous que cette règle soit utile ? Applicable ?
Avez-vous des exemples de code utilisant des boucles for qui ne pourraient pas être réécrits avec les algorithmes de la STL ?


Vous avez aimé cette actualité ? Alors partagez-la avec vos amis en cliquant sur les boutons ci-dessous :


 Poster une réponse

Avatar de r0d r0d - Expert éminent http://www.developpez.com
le 13/12/2012 à 11:24
Citation Envoyé par gbdivers  Voir le message
Et tu n'es pas obligé de passer par get pour utiliser une fonction membre

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:
Code : Sélectionner tout
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; 
}
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.
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):
Code : Sélectionner tout
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
Et pour 2. (avec pointeur nu):
Code : Sélectionner tout
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
On voit bien que, avec unique_ptr, on a un push en plus ( 00050 50 push eax).

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.
Avatar de gbdivers gbdivers - Inactif http://www.developpez.com
le 13/12/2012 à 11:25
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

Citation Envoyé par John Carmack
La plupart du temps, cela ne présente qu'un intérêt théorique, car nous échangeons tout le temps des performances contre de la productivité.

(source : Programmation fonctionnelle en C++)
Pourtant Carmack est bien le genre à aller chipoter dans le code pour faire des hacks permettant de gagner quelques cycles...
Avatar de r0d r0d - Expert éminent http://www.developpez.com
le 13/12/2012 à 11:30
Citation Envoyé par gbdivers  Voir le message
Autre chose, ça n'a pas de sens de parler de "quelques cycles" dans l'absolue, sans donner de référentiel.

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.
Avatar de gbdivers gbdivers - Inactif http://www.developpez.com
le 13/12/2012 à 11:32
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)
Avatar de gbdivers gbdivers - Inactif http://www.developpez.com
le 13/12/2012 à 11:37
Citation Envoyé par r0d  Voir le message
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.

Ok, dans ce cas, tu es dans la situation (qui arrive dans 0,01% des cas) où
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.

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éfaut
Avatar de r0d r0d - Expert éminent http://www.developpez.com
le 13/12/2012 à 11:39
Citation Envoyé par gbdivers  Voir le message
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)

Ha oui, tu as raison (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 : Sélectionner tout
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
Avatar de gbdivers gbdivers - Inactif http://www.developpez.com
le 13/12/2012 à 11:40
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)
Avatar de gbdivers gbdivers - Inactif http://www.developpez.com
le 13/12/2012 à 11:42
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.
Avatar de r0d r0d - Expert éminent http://www.developpez.com
le 13/12/2012 à 11:48
Citation Envoyé par gbdivers  Voir le message
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):
Code : Sélectionner tout
(&**p)->value;
Le "&**" venant de l'opérateur ->() de unique_ptr.

Là il y a "inlinisation" + optimisation.
Avatar de camboui camboui - Membre éclairé http://www.developpez.com
le 13/12/2012 à 12:08
Citation Envoyé par gbdivers  Voir le message

Désolé d'interrompre pour si peu...
Le lien est invalide.

EDIT gbdivers : lien corrigé, merci
Avatar de JolyLoic JolyLoic - Rédacteur/Modérateur http://www.developpez.com
le 13/12/2012 à 15:47
Citation Envoyé par r0d  Voir le message
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

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.
Offres d'emploi IT
Développeur web front end H/F
NEXTGEN RH - Aquitaine - Bordeaux (33000)
Développeur c#/sql
MacroProg - Ile de France - Paris (75000)
Sécurité de la donnée dans hadoop
Atos - Rhône Alpes - Grenoble (38000)

Voir plus d'offres Voir la carte des offres IT
Contacter le responsable de la rubrique Accueil