|
| Langage C++ : l'optimisation de code (1) | |
| | |
Suite à l'article Langage C : l'optimisation de code de Jean-Francois, je me suis enfin décidé à écrire un article sur l'optimisation de code en C++. Cela fait depuis un moment que cette idée me trotte dans dans la tête, et l'article de JF est une bonne excuse pour en faire un autre en parallèle, mais sur le C++.
Le sujet étant vaste, ce premier article ne va se concentrer que sur un problème bien précis en C++. Il n'est d'ailleurs pas question de se lancer dans des optimisations obscures et illisibles, mais bien de faire du code propre, lisible, et rapide. Bref, prenez cet article, avec plus ou moins de distance, comme une série de conseils pratiques en C++. |


| | |
Le terme 'opérateurs par défaut' est un abus de ma part et désigne en fait quatre fonctions membres d'une classe, automatiquement définies (si possible) par le langage, et donc par le compilateur :Constructeur par défaut Fonction qui construit un objet d'une classe, et qui peut être appelée sans avoir à passer des paramètres. Par exemple, le consructeur par défaut de la classe plop c'est plop().
Constructeur par copie Fonction qui construit un objet d'une classe à partir d'un autre objet de la même classe. Le constructeur par copie de la classe plop c'est plop(const plop &).
Destructeur Fonction appelée automatiquement lorsqu'un objet arrive en fin de vie. Elle sert généralement à faire le ménage au niveau des ressources. Le destructeur de la classe plop c'est ~plop().
Opérateur d'assignation Fonction qui assigne un object sur un autre. Elle est appelée automatiquement, par exemple, quand on fait a = b. L'opérateur d'assignation de la classe plop c'est plop & operator = (const plop &). Ces quatres fonctions membres d'une classe sont automatiquement définies si possible, c'est-à-dire si toutes les variables membres ont un opérateur correspondant qui peut être utilisé. Par exemple, si un objet de la classe plop encapsule deux objets a et b, la classe plop aura un destructeur automatiquement défini si a et b ont tous les deux un destructeur utilisable. |

| | |
En connaissant les parties qui sont définies automatiquement par le compilateur, on peut faire des classes plus légères et plus efficaces. Pour l'exemple, faisons une classe qui ne contient qu'un entier. En sachant que le compilateur va automatiquement s'occuper du constructeur par défaut, du constructeur par copie, du destructeur, et de l'opérateur d'assignation, une telle classe peut être implémentée de la manière suivante:

Il est difficile de faire plus simple. Mais une grande majorité de programmeurs l'implémentent de la manière suivante :

A première vue, cette deuxième version est identique à la première du point du vue du comportement de la classe. Mais quelles sont les erreurs commises ? -
Dans la classe bad_example, on retrouve bien dans l'orde: le constructeur par défaut, le constructeur par copie, le destructeur, et en dernier l'opérateur d'assignation. Ils font tous strictement la même chose que s'ils avaient été implicitement défini par le compilateur. C'est donc une erreur de les définir explicitement. Non seulement ca prend beaucoup de ligne de code pour rien (c'est redondant: cela rend le code illisible, et augmente sensiblement les chances d'introduire des bugs dont on se serait bien passé), mais cela ne sera jamais plus efficace que la solution produite par le compilateur (dans le meilleur des cas, ce sera aussi efficace, mais dans les cas contraires...)
Le lecteur attentif aura remarqué que les deux classes ne sont pas tout à fait équivalentes. En effet, le destructeur de bad_example est déclaré virtual, alors que le destructeur défini implicitement pour good_example ne l'est pas. De nombreux livres, ainsi que d'autres ressources sur le C++, conseillent de déclarer automatiquement tous vos destructeurs comme étant virtuels: cela évite d'oublier de le faire quand c'est vraiment utile, et donc évite des bugs obscures. C'est une bien mauvaise idée! Cela va à l'encontre du fameux 'Ce que vous n'utilisez pas ne peut vous faire du mal'. Utiliser un destructeur virtuel quand on n'en a pas besoin, va causer du mal (comme on le verra plus loin dans cet article). |

| | |
En théorie, tout ça est bien beau, mais cet article tourne autour de l'optimisation de code. Il est donc temps de passer à la pratique! Un petit ensemble de tests sur ces deux classes s'impose.
| | Integer | Good | Bad | Evil | Taille (bytes) | 4 | 4 | 8 | 8 | Allocation (millisecondes) | 240 | 240 | 520 | 560 | A = B * 2 (millisecondes) | 250 | 240 | 450 | 640 | Désallocation (millisecondes) | 30 | 30 | 390 | 380 | Résultats sur un Athlon XP 3000+ (2.1 GHz) 1024 MB avec GCC 4.0 Erreur : +- 10 msecs Quatre implémentations de la même classe sont passées en revue:Integer n'est en fait qu'un entier natif de type int; Good représente la classe good_example, vue plus haut; Bad représente la classe bad_example; Evil se base sur la classe bad_example, mais n'utilisepas de fonctions inline (histoire d'enfoncer le clou un peu plus).
Ces quatres implémentations passent par quatres tests :Taille Donne la taille d'un objet. Le but étant évidemment de voir si certains choix d'implémentation peuvent influencer ce point.
Allocation Ce test alloue un tableau de N objets.Il fait resortir l'influence de la taille des objets, ainsi que l'efficacité duconstructeur par défaut.
A = B * 2 Ce test multiplie la valeur de l'objet B par 2 et l'assigne dans A. Tel qu'il est implémenté, il met en avant l'efficatité de la création d'un objet temporaire (constructeur par copie et destructeur) et de l'assignation de cet objet sur un autre (opérateur d'assignation). -
Désallocation Ce test désalloue le tableau d'objets créés précédemment. Il met à mal l'efficacité de l'implémentation du destructeur, et est aussi influencé par la taille des objets. Un petit mot d'explication est peut-être nécessaire pour les résultats sur la taille des objets qui doublent avec Bad et Evil. Cette augmentation est due au destructeur virtuel; en effet, dès qu'une classe possède une ou plusieurs méthodes virtuelles, elle implémente un pointeur vers une table utilisée pour implémenter le méchanisme de polymorphisme (ce pointeur faisant 32 bits sur la machine de test). Pour le reste, les résultats parlent d'eux-même. Les sources utilisées pour ces tests sont disponibles ici. Vous y trouverez, notamment, les résultats obtenus avec d'autres compilateurs. Je vous invite à supprimer le destructeur virtuel et à en mesurer l'impact (qui est assez surprenant, suivant le compilateur utilisé). |



YOUM (analyseur syntaxique temps réel) | Nombre de définitions trouvées 19 Multi-dico par texte : actif - Multi-mots par définition : 4
|
|
|



 
|