Overhead d’une classe et de son constructeur C++ sur une structure C

Il y a quelques temps, il y a eu une discussion assez animée sur Developpez.com autour des avantages de C++ par rapport à C. Comme trop souvent, des gens ont exposés des croyances et des convictions sans fondement ou a minima pas complètement vérifiées. Je débute dans mon utilisation professionnelle du C++, je développe sur micro-contrôleurs relativement contraints et surtout j’aime bien les preuves. Quand il a été dit que les constructeurs C++ étaient très coûteux et nécessitaient des mécanismes complexes, j’ai tiqué : sans avoir vérifié, je ne voyais rien qui pouvait justifier cela a priori. Un constructeur n’est souvent qu’une fonction qui initialise des variables…

Je suis donc allé sur Compiler Explorer (1), un site fort sympathique où vous écrivez du code source à gauche et où le code assembleur correspondant est généré à la volée à droite. Vous pouvez choisir parmi un nombre important de compilateurs dans différentes versions, changer les options de compilation et surtout, des blocs de couleurs font la correspondance entre les lignes du code source et les instructions assembleur correspondantes.

Puisque je travaille sur des interfaces graphiques, j’ai choisi de modéliser un écran avec seulement 2 propriétés : hauteur et largeur. J’ai donc écrit plusieurs codes C et C++ et comparé les codes assembleur générés. Je vous partage les captures d’écran faites depuis Compiler Explorer, vous pouvez cliquez dessus pour les ouvrir en taille réelle.

Pour commencer, voici une classe C++ très basique et très classique :

1-cpp-classe-et-constructeur

On voit tout de suite qu’un constructeur ne génère pas des tonnes d’assembleur et n’utilise pas de mécanique complexe. Comparons tout de suite cela à une structure C avec une fonction d’initialisation :

2-c-struct-et-fonction-creation

C’est donc confirmé : non, un constructeur C++ ne coûte pas les yeux de la tête comparé à une fonction C. Ici, il y a un peu plus d’assembleur mais ça ne change pas la face du monde (2).

Le hic est que ce code C n’est pas très idiomatique : on n’a pas besoin d’une fonction pour créer et initialiser une structure en C…. Voici donc un second code avec la même structure initialisée de manière classique, ainsi qu’une nouvelle fonction d’affichage :

3-c-idiomatic

L’initialisation de la structure tient maintenant en 2 instructions assembleur, c’est beaucoup mieux. Remarquez au passage qu’un appel de fonction (même vide), ça coûte ! Il faut maintenant trouver une alternative crédible en C++. On ne va se trainer un constructeur C++ si coûteux comparé à une bête liste entre accolades en C ! La première idée est celle d’une classe template. On a généralement un seul écran donc la duplication de code liée à la spécialisation des templates n’est pas un problème et la classe sera optimisée à la compilation pour les dimensions de l’écran (bon OK, ici, la classe est vide…). Voici ma classe template et son code assembleur :

4-cpp-template

Le code assembleur est le même que celui de la version C ! WTF ?! Un template C++ ça ne coûte pas super cher ? Et ben non : un template n’est rien de plus qu’une sorte de super macro. Quand on l’instancie, le contenu est copié et les paramètres formels sont remplacés par les arguments de l’instanciation.

Il y a une autre alternative en C++ dans le cas où la duplication du code n’est pas souhaitable : l’aggregate initialization. Pour que cela soit possible, il faut que la classe respecte certaines contraintes, notamment celui que ces champs soient publiques. On a alors un code source comme celui-ci, pour le même code assembleur généré :

5-cpp-aggregate-type

Pour conclure, on voit qu’une classe C++ simple n’est pas vraiment plus coûteux qu’une simple structure en C. Il y a des mécaniques C++ coûteuses, comme les fonctions virtuelles, les exceptions, mais vous n’êtes pas obligés de les utiliser si vous avez de vraies contraintes de tailles et de performances. On peut faire du code en C++ qui a des performances et une empreinte mémoire semblable à un code C, mais le C++ vous apporte des choses intéressantes que vous n’aurez pas en C. Le sujet de cet article n’est pas de lister ces choses intéressantes mais bien de montrer qu’il ne faut pas avoir d’a priori, dans un sens comme dans l’autre, et qu’il est nécessaire de tester, de se documenter et de vérifier.

(1) J’ai découvert ce site en regardant la vidéo de la conférence de Jason Turner à la CppCon 2016. Il montre comment il a codé le jeu pong sur Commodore64 en en C++17. Il utilise les fonctionnalités les plus avancées du langage et montre l’assembleur au fur et à mesure. Cette vidéo est incroyable et détruit le mythe qui voudrait que le C++ moderne et évolué ne permet pas de produire du code compact ! À regarder absolument !

(2) Vous remarquerez que les codes ci-dessus ont été compilés en -O0, c’est-à-dire sans optimisation. Dés que les optimisations sont activées, à partir de -O1, tous ces codes produisent (naturellement ?) l’assembleur suivant :

main:
mov eax, 560
ret
Publicités

4 Réponses

  1. Faire ce genre de test sans les optimisations n’a pas grand intérêt. Si on ne veut pas optimiser, il faut tout écrire en assembleur à la main!

    En activant les optimisations qui vont bien (-Os si on parle d’embarqué avec des contraintes d’espace mémoire par exemple), et en faisant un peu attention au code qu’on écrit, il est plus facile d’avoir un code propre et lisible en C++ qu’en C, pour un code assembleur équivalent voire plus performant.

    Forcément, quand on fait une classe avec une méthode, on se dit « bof ». Mais quand on a vraiment des objets à manipuler, comme c’est probablement le cas quand on fait une GUI sur un écran, avoir un langage haut niveau permet de factoriser plus facilement le code. Alors bien sûr, le C pourra toujours faire aussi bien en écrivant du code très moche qui va ressembler de plus en plus à ce que le compilateur C++ aurait fait.

    Moi en ce moment je suis sur un projet en C (à mon grand malheur), et on a fini par se refaire un framework d’objets avec des vtable et tout le bazar. C’est clairement pas aussi bien optimisé que ce que ferait un compilateur C++, et c’est moins lisible.

    En C++, on a un langage plus riche pour dire plus précisément ce qu’on veut au compilateur. On peut faire des coroutines – de façon lisible et sans utiliser d’extension gcc (http://aldrin.co/coroutine-basics.html – je te laisse trouver l’équivalent en C que même moi j’ai jamais osé utiliser :o)), générer du code avec des templates pour remplir sa flash mais économiser sa ram, où au contraire écrire un petit bout de code générique qui fait plein de trucs en s’applicant à plusieurs objets différents en RAM. On peut déclarer un objet « const » pour qu’il soit stocké en flash, et une fonction « constexpr » pour dire qu’elle DOIT être résolue en une valeur simple à la compilation. Avec tout ça, ça devient difficile de faire aussi bien avec juste du C.

    J'aime

    24 octobre 2016 à 9:13

  2. Hello,

    Ça t’avait manqué que je blogue ? T’es au taquet pour commenter !

    Je sais que cet exemple est trop simple et qu’en effet, en -O0, c’est pas fou fou. Mais ça montre quand même que le C++ ne génère pas des tonnes d’assembleur, c’était l’idée toute bête derrière cet article.

    Quand t’en es arrivé à refaire un mécanisme avec des vtables, pourquoi ne pas passer en C++ ? Pas de compilateur dispo pour la cible ? Volonté politique ?

    Je te sens encore plus pro C++ qu’il n’y a quelques années en tout cas ; )

    T’as regardé la vidéo ? Je l’ai ajoutée à l’article largement pour toi !!!

    J'aime

    26 octobre 2016 à 7:56

    • pulkomandy

      Le -O0, c’est un peu enlever la moitié du compilateur. c’est intéressant de regarder ce qu’il se passe dans les couches intermédiaires, mais en fait ce qui est surtout intéressant, c’est de voir comment l’optimiseur arrive très bien à s’en sortir. Sur un exemple simple comme le tien, le C++ n’est même pas « un peu plus gros » que le C. Quelle que soit la façon d’écrire le code, à la fin, avec les optimisations, on a le même binaire de deux instructions. Et ça, c’est super rassurant: ça veut dire que le compilateur fait bien son travail, et qu’on peut écrire du code d’abord lisible pour les humains. Au compilateur de se débrouiller pour en faire un truc performant.

      Je met la vidéo dans un coin pour quand j’aurais le temps 🙂 (mais je savais pas qu’il y avait un compilateur C++ pour le CPU 6502, il va falloir que j’aille me renseigner là dessus).

      Pour mon projet, on a demandé si on pouvait faire du C++, la réponse a été « pas le temps ». Ce qui est vrai, tout le monde n’est pas forcément performant dans ce langage et c’est déjà un peu galère pour trouver des gens qui maîtrisent bien le C.

      J'aime

      26 octobre 2016 à 9:00

  3. C’est quelque chose que beaucoup de gens sous-estiment mais qu’on peut lire souvent dans des articles sur l’optimisation : en écrivant du code simple, le compilateur peut faire du meilleur boulot. En effet, les gens qui écrivent des compilateurs sont des humains, ils reconnaissent des patterns de code et écrivent des optimisations pour ces patterns. Un code incompréhensible, c’est un risque que le compilateur ne reconnaisse pas un schéma classique à optimiser et du coup l’optimise peu.

    Spoiler : il n’y pas de compilateur natif ; )

    Je comprend cet argument. A ceci près qu’avec un compilateur C++, on peut faire C (presque) classique, impeccable pour les dév ne maitrisant pas le C++, et aussi du C++, pratique pour les trucs avancés et ainsi éviter de refaire des vtables !

    J'aime

    26 octobre 2016 à 6:11

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s