Le blog d'un geek devenu directeur technique

Billets dans la catégorie Questions techniques

Fonctionnement interne des langages de programmation

Juste pour le fun, voici quelques liens vers des sites qui détaillent le fonctionnement interne de plusieurs langages de programmation. C’est très intéressant à étudier.

Si vous connaissez d’autres sources d’information de ce type, n’hésitez pas à les ajouter dans les commentaires.

PHP
PHP Internals Book

Perl
Perl 5 Internals
Parrot

Lua
The Implementation of Lua 5.0 (PDF)
The Virtual Machine of Lua 5.0 (PDF)
A No-Frills Introduction to Lua 5.1 VM Instructions (PDF)

Java
The Structure of the Java Virtual Machine
JVM Internals blog

Python
Design of CPython’s Compiler
Python internals: Working with Python ASTs
Python’s Innards

Ruby
Ruby Under a Microscope
Ruby Internals (70 slides)

Dart
The Essence of Google Dart: Building Applications, Snapshots, Isolates

Javascript
The V8 JavaScript Engine
V8 Internals: Building a High Performance JavaScript Engine

.NET / CLR
Internals to .NET
Drill Into .NET Framework Internals to See How the CLR Creates Runtime Objects
.NET Type Internals – From a Microsoft CLR Perspective

Forth
A sometimes minimal FORTH compiler and tutorial for Linux (part 2)

Autres
The Potion Language

Edit : J’ai ajouté des liens Lua et Forth qui m’ont été transmis par @pchapuis. Encore merci !  :-)

Appel à avis : Handlers d’événement inline en Javascript

À la suite de mon dernier article, au sujet des technos navigateur, je prépare un autre billet consacré aux langage alternatifs qui génèrent du Javascript. Et au détour de tout ça, je suis tombé sur une page de la documentation Dart qui m’a un peu interloqué.

Cette page, consacrée à l’intégration de Dart dans du HTML, parle entre autre des handlers d’événement inline (désolé pour le franglais, je n’ai pas trop de synonyme correct qui me vienne spontanément à l’esprit). Vous savez, c’est le fait de mettre un “onclick” ou un “onchange” sur un élément HTML pour appeler une fonction Javascript lorsque l’événement en question est exécuté.

With JavaScript, programmers can embed inline event listener code directly onto HTML nodes. However, this is typically discouraged in modern JavaScript applications. HTML pages generally render more quickly if listeners are added programmatically afterwards. Modern security guidelines also discourage inline code. As such, we propose to not allow inline Dart listeners.

J’ai été un peu étonné de lire ça. En cherchant 30 secondes sur le web, je suis tombé sur une question/réponse du site StackOverflow :

Most experienced developers shun this method, but it does get the job done; it is simple and direct.

Je fais donc appel à l’aide des lecteurs de ce blog.
Je code pas mal de Javascript ; pas autant que de PHP, mais ce doit être au final mon deuxième langage en quantité de code écrit (eh oui, même devant le C). J’écris du code que j’estime propre, en créant des objets et en les rangeant dans des namespaces, utilisant des callbacks là où ça se justifie, tout en se gardant du « callback hell ».

Et personnellement, j’ai toujours trouvé qu’il était plus facile de maintenir du code avec les appels d’événements indiqués dans le HTML, plutôt que de les positionner par Javascript. Quand on cherche ce qu’il se passe quand on clique sur tel élément, ou quand on tape du texte dans un champ texte, il est très facile de regarder le code HTML, voir quel méthode de quel objet est appelée, et enfin aller voir directement dans cette méthode.

Donnez-moi votre avis s’il-vous-plait.

Préférez-vous ce type de code :

<script>
var site = new function() {
    this.init = function() {
        // quelques initialisations
    };
    this.checkContent = function(text) {
        // un peu de code applicatif
    };
};
</script>
<input type="text" name="content"
 onblur="site.checkContent(this.value)" />

Ou plutôt celui-ci :

<script>
var site = new function() {
    this.init = function() {
        // initialisation du handler d'événement
        $("#edit").on("blur", function() {
            site.checkContent();
        });
    };
    this.checkContent = function() {
        var text = $("#edit").val();
        // un peu de code applicatif
    };
};
</script>
<input id="edit" type="text" name="content" />

Et pourquoi ?

Dart, NaCl, Pepper, Emscripten, PNaCl : Du nouveau du côté de la programmation sur navigateur

Comme tous ceux qui font du développement web, je code pas mal en Javascript. J’en ai déjà parlé sur ce blog, c’est un langage dont j’apprécie la souplesse mais dont les limitations m’empêchent d’imaginer faire du vrai génie logiciel dessus. J’ai beau faire des développements JS “propres”, avec l’utilisation d’objets rangés dans des namespaces hiérarchiques, je ne cesse de pester contre l’âpreté de son modèle objet.

Toutefois, pas mal de choses bougent en ce moment du côté du développement sur les navigateurs. Je n’ai pas encore vraiment mis le nez dans tous ça − à part lire de la documentation − mais ça reste intéressant d’en parler un peu.

Dart

Pour commencer, le langage Dart − créé par Google pour remplacer un jour le Javascript − vient de voir son SDK publié en version 1.0. C’est un langage qui fait voler en éclat les problèmes du Javascript ; son modèle objet est complet, il gère nativement les packages, et il peut être aussi bien fortement que faiblement typé.

Pour le moment, seule une version spécialement modifiée du navigateur Chromium (Dartium) est capable d’interpréter directement du code Dart. Mais il est possible de « compiler » du code Dart en code Javascript, permettant son exécution par n’importe quel navigateur. Là où ça devient intéressant, c’est que cette phase permet d’optimiser le code Javascript généré au point de le rendre en 42% et 130% plus rapide que le même code écrit nativement en Javascript.
C’est d’autant plus intéressant que les interpréteurs Javascript ont connu une énorme augmentation de leurs performances ces dernières années.

Bon, par contre je ne suis pas persuadé que passer par une phase de compilation pour du code client soit très pratique d’utilisation. Mais il reste toujours la possibilité de développer sur Dartium, puis de tester le code JS généré sur les autres navigateurs.

En tout cas, si le Go (l’autre langage de Google) a réussi à obtenir un peu de “traction”, j’ai le feeling que le Dart a le potentiel pour se faire une place de choix dans l’univers des langages de programmation modernes.

Programmation C/C++

Bon, quand il s’agit d’obtenir les meilleures performances possible, on finit quoi qu’il en soit par programmer en C ou en C++. Les ingénieurs de Google en étaient arrivés à cette conclusion et ont développé NaCl (pour “Native Client”) qui est une techno permettant d’exécuter dans un navigateur du code compilé pour processeur x86.
Enfin, quand on dit « dans un navigateur »… ça marche tant que le navigateur en question est Chrome, hein.

En plus, avec l’API Pepper, le code C/C++ n’est plus cantonné à une simple fenêtre, mais il peut communiquer avec le navigateur, ça peut amener à des choses sympatiques.

Mais fondamentalement, j’ai toujours trouvé cette idée bizarre. Oui, c’est génial d’avoir Quake qui tourne dans un navigateur web. Mais le principe fondamental du web, c’est de reposer sur des standards multi-plateformes. Embarquer du code compilé nativement pour un type de processeur au milieu de tout ça, c’est moche.

Ça tombe bien, car deux technologies différentes permettent de contourner ce problème. Les deux reposent sur une particularité du compilateur LLVM. Pour schématiser grossièrement, un front-end prend en charge la compréhension d’un langage de programmation, génère un bytecode spécifique, qui est ensuite utilisé par le back-end pour produire un exécutable natif. Et donc, le bytecode intermédiaire n’est pas dépendant du processeur sur lequel il va être exécuté.

La première techno basée sur ce bytecode s’appelle Emscripten. Elle prend du bytecode LLVM, et génère du code Javascript qui peut être exécuté directement par le navigateur. Le résultat est assez bluffant, dans la mesure où le moteur Unreal 3 a été porté en seulement 4 jours, et que la démo Epic Citadel qui est basée dessus est vraiment impressionnante.
En plus, avec la bibliothèque pepper.js, il est possible d’atteindre le même résultat qu’avec l’API Pepper (intégrée à NaCl, citée plus haut).

La seconde techno, issue de Google (décidément !), s’appelle PNaCl (pour Portable Native Client). Grosso modo, vous prenez NaCl, vous lui ajoutez Emscripten, vous mélangez bien, et voilà le résultat. En fait, le navigateur intègre un interpréteur de bytecode LLVM. On obtient ainsi le meilleur de chaque monde : la vitesse d’exécution qu’on est en droit d’attendre d’un code C/C++, avec la portabilité qu’on est en droit d’exiger sur le web.

Conclusion

Comme je le disais, ça bouge du côté du développement sur navigateur. Et c’est assez excitant. Je vais avoir du mal à trouver le temps d’expérimenter ces technos, mais j’en ai bien envie.

On peut remarquer que le vénérable Javascript, grâce à son support quasi-universel, reste le garant de la compatibilité de ces technologies (Dart, Emscripten) avec tous les navigateurs existants. Il devient le bytecode générique du web, un peu comme le C est utilisé par certains langages exotiques comme un bytecode intermédiaire (j’en parlais dans un article sur la création d’un interpréteur).
Pas sûr qu’il soit très simple ni rapide de faire une triple compilation (C/C++ vers bytecode LLVM, puis vers Javascript, pour enfin être interprété sur le navigateur, éventuellement avec de la compilation JIT). Mais si on peut accélérer le codage en développant sur un navigateur spécifique, pour ensuite être compatible avec tous les autres, ça peut en valoir la chandelle.

Plates-formes de développement privilégiées

Je réfléchissais dernièrement à une chose un peu particulière : le fait que plusieurs plate-formes informatiques ont eu des environnements de développement privilégiés, qui en sont devenus plus ou moins indissociables.

Psion – OPL

C’est en fait le couple auquel je pensais initialement, et qui m’a amené à écrire cet article. Le langage OPL (Open Programming Language) est un dérivé du Basic que la société anglaise Psion a intégré à ses ordinateurs de poche. Au milieu des années 80, ces machines n’étaient pas très sexy, et ressemblaient plutôt à des calculatrices améliorées, mais elle n’en étaient pas moins de vrais ordinateurs programmables.

Psion Organiser

Mais c’est avec les Psion série 3 que les choses prirent une dimension différente. Cette machine a été lancée en 1991 puis a été améliorée durant toute la décennie, avec des améliorations concernant la taille de l’écran, la puissance du processeur ou la mémoire embarquée (voir des copies d’écran du système).

J’ai personnellement possédé un Psion 3a pendant la seconde moitié des années 90. J’avais été marqué par la sortie du premier modèle quelques années auparavant. C’était le premier véritable ordinateur de poche.

Psion Series 3a

Plusieurs applications étaient embarquées : base de données, traitement de texte, tableau, agenda, … Mais surtout, ce qui a fait le succès de cet ordinateur à mes yeux, était la facilité avec laquelle n’importe qui pouvait créer de nouveaux programmes et les diffuser. Et cela était possible grâce à l’intégration du langage OPL et d’un environnement de développement sommaire mais fonctionnel dans le système.

Psion avait même eu l’intelligence de proposer des émulateurs de ses machines, qui tournaient sous MS-DOS. Ils permettaient de développer en utilisant un clavier plus pratique que celui des machines de poche. Ces émulateurs restent un bon moyen de faire revivre ces systèmes, grâce à DOSBox (voir ce lien).
Néanmoins, une énorme quantité de programmes ont été créés directement sur des Psion Série 3, et distribués par les différents moyens de l’époque (disquettes ou CD dans les magazines, puis internet).

Pourquoi l’OPL était-il si intéressant ? Parce qu’il était simple à apprendre − c’était un dérivé du Basic, en plus puissant pour l’époque − et qu’il proposait des routines graphiques faciles à mettre en œuvre, permettant de créer des interfaces graphiques sans trop de difficultés.

Pour la petite histoire, Psion a proposé par la suite un ordinateur plus performant, le Série 5, là encore avec l’OPL intégré. Puis le système du Psion Série 5 a servi de base au système Symbian, qui a longtemps été au cœur des téléphones Nokia et Ericsson.

Macintosh – HyperCard

L’exemple le plus marquant est sûrement le couple formé par HyperCard et le système d’exploitation des Macintosh à partir de 1987 et durant les années 90.

L’ordinateur qui innovait le plus à l’époque (en dehors de l’Amiga, bien sûr − petit troll gratuit), notamment par la simplicité d’utilisation de son interface graphique, aurait pu rester une machine difficile à programmer, que seule une élite aurait pu enrichir de leur logiciels. Mais le génie de Bill Atkinson a créé un outil qui mélange à la fois le logiciel de dessin, la base de données, le langage de programmation et l’hypertexte.

Pour comprendre comment fonctionnait HyperCard, vous pouvez regarder la vidéo proposée sur le site hypercard.org :

Je ne sais pas s’il existait déjà des outils de programmation WYSIWYG, mais HyperCard a clairement démocratisé cela.

Windows – Visual Basic

Autre cas emblématique, le Visual Basic qui est devenu l’un des environnements de développement les plus utilisés au monde, dont la première version a été lancée sous Windows 3 au début des années 90 (1991 pour être exact).

Lancé 4 ans après HyperCard, il en reprenait les principes de programmation événementielle sur la base d’une interface graphique créée à la souris, en y ajoutant des concepts provenant du couple Project Builder / Interface Builder présenté en 1988 sous NeXTSTEP.

Visual Basic

Le succès a commencé avec la version 3, qui s’appuyait elle-même sur le succès de Windows 3.1.

Pour avoir programmé en VB au milieu des années 90, je peux affirmer qu’un tel langage fait prendre plein de mauvaises habitudes quand on ne sait pas encore vraiment développer. Par contre, il permet de créer facilement et rapidement des applications relativement complexes, et son succès n’est absolument pas volé.

Mac OS X – XCode

Héritier spirituel du Project Builder déjà cité, XCode est l’environnement de développement offert gratuitement par Apple aux possesseurs de Mac. L’un de ses grands intérêts est de servir à la fois pour développer des applications pour Mac OS X, mais aussi pour iOS.

© Apple

 © Apple

Sa gratuité et sa qualité générale en ont fait l’outil incontournable pour tous les développeurs Mac et iOS.

Et alors ?

Tout ça, c’est bien beau, mais à part l’aspect historique des choses, ça sert à quoi ?

En fait, j’ai deux réflexions qui me viennent à l’esprit.

La première, c’est que toute plate-forme informatique devrait fournir des outils de création gratuits et facile à prendre en main (on peut argumenter que le Visual Basic n’est pas gratuit, mais on connait aussi le taux de piratage des logiciels sous Windows, hein). Je ne parle pas d’outils ultra-pointus ; même quelque chose de limité permet toujours à des gens d’exprimer leur créativité. Je pense évidemment à des outils de développement ; mais c’est aussi valable pour de la création graphique (dessin bitmap, vectoriel, 3D), musicale, …
Il y a toutefois une légère différence : offrez un logiciel de dessin, et vous aurez des dessins ; offrez un outil de programmation et vous aurez des logiciels de dessins (et des dessins), des logiciels de musique (et des chansons), des éditeurs web (et des sites web), …

La seconde réflexion que je me suis faite, c’est qu’il faut toujours permettre aux gens de créer directement sur la plate-forme à laquelle est destinée les programmes qui sont développés. D’un côté l’exemple du langage OPL sur les Psion montre que cela a permis à tous les utilisateurs de ces machines de créer des programmes, sans attendre que des logiciels commerciaux remplissent tous les besoins.
De l’autre côté, je pense à GeoWorks Ensemble, une surcouche graphique de MS-DOS concurrente de Windows 3.x (apparue en 1990, soit 1 an avant Windows 3.0). Cette plate-forme était d’une qualité incroyable (multitâche, polices vectorielles, rapidité incroyable, …), mais nécessitait une station de travail Sun Sparc hors de prix pour développer des logiciels dessus. Échec commercial.

Je pense réellement que les terminaux de consommation passifs sont une chose qui ne devrait pas exister. Évidemment, la plupart des utilisateurs veulent pouvoir lire, regarder, écouter des média diffusés sur leurs appareils ; mais cela ne doit pas empêcher les autres utilisateurs de créer facilement et rapidement de nouveaux produits et services pour ces appareils.

Le futur ?

Ce qui m’embête un peu, c’est que j’ai donné des exemples plutôt anciens (à part XCode). L’informatique a évolué ces dernières années. Netbooks, ultraportables, tablettes et téléphone portables nous ont fait entrer dans une ère où l’ultra-mobilité règne.

Je trouve dommage que les éditeurs de systèmes pour tablettes imposent tous des SDK officiels qui s’utilisent uniquement sur ordinateur. Même si le SDK Android fourni par Google a l’avantage d’être multi-plateformes, il ne permet pas de coder directement sur une tablette Android.

Mais est-ce qu’un appareil mobile ne peut pas servir au développement ? L’exemple du Psion montre que si. D’ailleurs, il existe des solutions comme Codea (iOS) ou Droid Develop (Android) qui l’autorisent, et quand on regarde la vidéo de présentation de Codea, on se dit qu’il y a moyen de faire de très belles choses directement sur une tablette :

Pourtant, je reproche à ces solutions d’être encore trop proche du code. Il faut apporter des outils permettant de créer. Facilement. Directement sur l’appareil.

Est-ce que vous avez en tête d’autres exemples de plate-forme liée à une solution de développement privilégiée ?

FineDB : Gestion des timeouts

Si vous avez lu mon dernier article, consacré à FineDB, vous savez que l’une des évolutions que j’avais en tête était d’ajouter un thread dont le rôle aurait été de surveiller les connexions qui duraient depuis trop longtemps. Une manière de gérer les inactivités et de couper les connexions inutiles.

J’ai fait une implémentation complète de ce système. Cela imposait quelques modifications au code existant :

  • Gérer une option supplémentaire, pour définir la durée de timeout (avec une valeur par défaut à 90 secondes).
  • Ajouter un timestamp dans chaque thread, contenant la date de dernière activité − lecture ou écriture − du thread.
  • Créer un thread supplémentaire, qui parcours la liste des threads à intervale régulier. Ceux dont l’inactivité dure depuis un temps supérieur au timeout voient leur connexion coupée.

J’ai rencontré un premier problème. Lorsqu’une connexion était détectée comme trop longue, la socket équivalente était fermée avec la fonction close(). Malheureusement, cela n’est pas suffisant ; le thread de communication restait bloqué à son appel à la fonction read(). Le fait de fermer la socket avec close() envoie l’information au client situé à l’autre bout de la connexion, mais cela ne change rien pour le read() local, qui attend toujours de recevoir des données.

L’astuce est de faire un appel à shutdown() au lieu de close(), en lui donnant le flag SHUT_RDWR. En fermant d’un coup les « deux côté » de la socket (c’est-à-dire autant en lecture qu’en écriture), read() sort en retournant une erreur, ce qui permet de gérer le soucis.

En parallèle de ça, Armel Fauveau m’a conseillé via Twitter d’utiliser TCP keepalive, pour éviter d’avoir un tel thread de garbage collecting. Malheureusement, cette technique ne permet que de vérifier que la connexion est toujours établie entre le client et le serveur ; si le client est parti dans les choux (ou qu’il a simplement décidé de ne rien faire et de consommer une connexion pour rien), il n’y a aucune différence.

Mais la suggestion semblait censée, alors j’ai voulu creuser la question. Il avait raison, ce serait étonnant que rien n’existe au niveau réseau pour gérer les timeouts. J’ai donc trouvé, c’est au final assez simple : la fonction setsockopt() avec les options SO_RCVTIMEO et SO_SNDTIMEO. La littérature sur la question n’est pas débordante, mais le minimum vital est assez facile à trouver.

L’une des choses à laquelle il faut faire attention, quand on utilise ces options, est de ne pas les activer en continue. Imaginons que l’on place un timeout de 10 secondes, aussi bien en lecture qu’en écriture. Le serveur va passer le plus clair de son temps à attendre des données. Une fois qu’il les aura reçues, le compteur de timeout en lecture va repartir de zéro ; mais celui d’écriture continuera à tourner. Donc si le temps d’attente suivi du temps de traitement des données est supérieur au timeout d’écriture, la socket va être fermée alors que d’un point de vue applicatif tout se déroule comme prévu. De la même manière, après avoir reçu des données, le temps de les traiter puis d’envoyer un retour au client peut être plus long que le timeout de lecture.
Là, le truc est de placer le timeout sur la socket juste avant l’opération système bloquante (read() ou write()), puis de le retirer juste après. Ainsi, on s’assure que ce ne sont que les délais de communication client-serveur qui sont pris en compte par les timeouts.

Je viens de mettre à disposition une version du code qui implémente cette évolution.

Au passage, les évolutions de FineDB sont listées sur le site du projet.
Pour ceux que ça intéresse, j’ai ajouté une commande PING au protocole, permettant de tester si une connexion au serveur est toujours effective. Je l’utilise dans l’outil en ligne de commande, pour tester automatiquement la connexion avant l’envoi d’une requête, pour reconnecter automatiquement si nécessaire (c’est une option désactivable).
Pour le reste, les commandes de base sont toutes implémentées, y compris le support des transactions. La bibliothèque cliente et l’outil en ligne de commande sont à jour par rapport aux fonctionnalités du serveur.

FineDB : Architecture interne

J’ai lancé le projet FineDB il y a quelques semaines. Je vais expliquer comment fonctionnent les mécanismes internes du serveur.

Pour info, le projet a un site dédié : finedb.org

Threads

Il existe 3 types de threads dans le serveur FineDB :

  • Le thread principal, qui crée les autres threads puis écoute les nouvelles connexions venant des clients.
  • Le thread d’écriture, dont le rôle est d’écrire dans le moteur de stockage les données qui doivent être écrites de manière asynchrone.
  • Les threads de communication. Ils gèrent les échanges avec les clients, en interprétant leurs requêtes. Les lectures et les écritures synchrones sont faites directement ; les écritures asynchrones sont déléguées au thread d’écriture.

Communication entre les threads

Il y a deux flux de communication nanomsg à l’intérieur du serveur.

Le premier est un flux de type “fanout” PUSH/PULL load-balancé. Le thread principal y envoie les files descriptors correspondant aux sockets des connexions entrantes. Nanomsg distribue automatiquement ces messages aux threads de connexion, qui peuvent ainsi entamer la communication avec les clients.

Le second est un flux “fanin” SOURCE/SINK. Les threads de communication l’utilisent pour envoyer des ordres au thread d’écriture.

Schéma

  1. Une socket TCP classique attend de nouvelles connexions entrantes.
  2. Connexion nanomsg utilisée pour transmettre les connexions entrantes aux threads de connexion.
  3. Les opérations de lecture sont faites directement sur le moteur de stockage.
  4. Les opérations d’écritures synchrones sont faites directement sur le moteur de stockage.
  5. Connexion nanomsg, utilisée pour transmettre les ordres d’écriture asynchrone au thread d’écriture.
  6. Le thread d’écriture accède directement au moteur de stockage.

État des lieux

Tout ce que j’ai décrit ci-dessus est déjà implémenté sous cette exacte forme.

Cette architecture est le fruit d’un choix. Le fait de passer par des threads qui prennent en charge les connexions entrantes est différent de la mode actuelle qui se base plutôt sur de la programmation événementielle mono-processus (à base de libevent ou libev).
Le grand avantages est une simplification importante du code, grâce à son découplage. Le thread principal écoute les nouvelles connexions, et une fois qu’elle sont établies il les envoie aux threads de connexion. Nanomsg facilite grandement cette communication, en jouant au passage les rôles de load-balancer et de file de message. Les threads de connexion ont pour seul tâche de lire les requêtes, les comprendre et les exécuter.
Il faut voir aussi que la programmation réseau mono-processus fonctionne bien tant qu’aucune opération bloquante ne peut ralentir l’ensemble des traitements. Dans le cas de FineDB, certains traitements (compression/décompression des données, accès au moteur de stockage) peuvent s’exécuter rapidement mais quand même trop lentement pour vouloir prendre le risque d’ajouter de la latence sur toutes les autres connexions.

L’inconvénient principal est que le nombre de requêtes traitées simultanément est égal au nombre de threads de connexion. Les connexions supplémentaires doivent attendre qu’une connexion se termine et que le thread devienne libre pour en prendre une nouvelle en charge.
Cet inconvénient est connu et assumé. Les threads sont très légers et un grand nombre peut être créé.

Évolutions futures

L’architecture actuelle va évoluer avec deux étapes.

La première, assez simple, consistera à gérer les connexions longues, pour les couper au bout d’un certain délai. Cela nécessitera un thread supplémentaire, qui parcourra à intervalle régulier la liste des connexions ouvertes et fermera celles qui n’ont pas montré signe d’activité depuis un certain temps.

La seconde sera plus complexe. Elle va servir à mettre en place les capacités de réplication (maître-esclave et maître-maître) de FineDB. J’en reparlerai dans un autre article, mais cela nécessitera d’ajouter encore un thread supplémentaire, qui gérera les interactions avec les autres serveurs.

Une autre piste d’amélioration serait de mixer programmation réseau événementielle et threads de traitement. Ainsi, le thread principal gérerait à lui seul les connexions ouvertes, mais confierait aux threads les opérations bloquantes. À voir si cela est nécessaire, en fonction des benchmarks que je vais mener ; cela demanderait une grosse réécriture du code.

Code défensif et sur-optimisation de code

Récemment, en faisant des revues de code avec mes développeurs, j’ai eu avec eux des discussions intéressantes que j’ai envie de partager sur ce blog.
L’un d’eux utilisait une pratique que j’appelle le code défensif. L’autre avait des idées d’optimisation de code qui étaient de la sur-optimisation. J’ai pris le temps de leur expliquer en quoi ces pratiques sont néfastes.

C’est quoi le code défensif ?

Ce que j’appelle du code défensif, c’est du code qui se défend contre le développeur qui l’a codé et qui va l’utiliser.

Je ne parle pas de l’écriture de bibliothèques qui sont destinées à être (ré-)utilisées dans des contextes variés et par des personnes très différentes. Dans ce cas-là, il faut au contraire prévoir le maximum de cas d’échec, et partir du principe que la bibliothèque est une « boîte noire » à laquelle on est susceptible de fournir des données complètement erronées.

Non, mon propos concerne du code qui « se connait » (si je puis dire). Un contrôleur qui appelle un objet métier dont il est le seul utilisateur. Une méthode publique qui appelle une méthode privée du même objet. Une fonction Javascript qui manipule un élément DOM de la page sur laquelle elle est présente.

C’est quoi le problème avec le code défensif ?

On pourrait se dire que c’est « mieux » de vérifier systématiquement les entrées d’une fonction. C’est plus « propre », c’est plus « sérieux ».

Mais il ne faut oublier que cela a deux impacts.
Le premier impact est peut-être le moins important : Toutes ces vérifications prennent du temps. Oh, pas grand-chose, bien sûr. Mais à force de vérifications inutiles, on finit par dégrader les performances d’une manière qui peut être perceptible.

Le second impact est bien plus grave : Ajouter des vérifications, cela veut dire ajouter du code. Plus de code veut dire plus de maintenance. Le code est moins lisible, il est plus difficile de faire évoluer la partie applicative, car il faut déjà la séparer du reste. Et quand on fait évoluer les fonctionnalités, il faut faire évoluer les vérifications initiales.

Encore une fois, il y a des situations où cela se justifie, et d’autres où c’est complètement inutile.

Quelques exemples

Imaginons un objet qui peut être utilisé de plusieurs manières. On va vérifier les entrées-sorties publiques, mais le développeur est censé savoir ce qu’il fait à l’intérieur de l’objet.

class AjouteEnBaseDeDonnees {
    public function ajoutEntier($i) {
        if (!isset($i) || !is_int($i))
            throw new Exception("Mauvais paramètre.");
        $this->_ajoute($i);
    }
    public function ajouteFlottant($f) {
        if (!isset($f) || !is_float($f))
            throw new Exception("Mauvais paramètre.");
        $this->_ajoute($f);
    }
    private function _ajoute($n) {
        // code défensif complètement inutile
        if (!isset($n) || (!is_int($n) && !is_float($n)))
            throw new Exception("Mauvais paramètre.");
        // ... ajoute la valeur en base de données
    }
}

Dans un cas comme celui-là, on voit bien qu’il est inutile de remettre dans la méthode privée les vérifications qui doivent être faite au plus tôt lors des appels à cet objet.

Autre exemple, une fonction Javascript qui manipule les éléments d’une page.

<html>
<head>
    <script type="text/javascript">
        // on part du principe que jQuery est chargé
        function afficheDate() {
            var panel = $("#panel-date");
            // code défensif inutile
            if (!panel[0]) {
                alert("Je ne peux pas écrire la date !");
                return;
            }
            panel.html((new Date()).toDateString());
            panel.show();
        }
    </script>
</head>
<body>
    <div id="panel-date" style="display: none;"></div>
    <div onclick="afficheDate()">Affiche la date</div>
</body>
</html>

On est d’accord, personne ne code réellement ce genre de chose. On fait des objets Javascripts qui sont proprement rangés dans des fichiers, qui sont eux-même nommés en fonction du namespace de ces objets. Mais ça reste valable dans l’idée.
Dans cet exemple, le code défensif est doublement inutile. Non seulement le développeur qui code la page est censé savoir ce qu’il fait, mais en plus jQuery gère les erreurs silencieusement. Honnêtement, pour que ce code ne fonctionne pas, il faut vraiment que le développeur ne teste pas sa page une seule fois.

La sur-optimisation de code

L’autre cas intéressant est la sur-optimisation. On comprend tous instinctivement qu’il ne faut pas optimiser inutilement. Mais il semblerait qu’on ne réussisse pas tous à sentir quand quelque chose relève de la sur-optimisation.

Pour commencer, revenons rapidement sur les problèmes générés par la sur-optimisation. Ils peuvent être de deux ordres.
Le principal soucis est similaire à celui du code défensif. Écrire plus de code implique de maintenir plus de code ; cela entraîne des difficultés pour le faire évoluer. Le code est plus dur à débugguer et plus lent à améliorer. Ces inconvénients ne peuvent pas être négligés, car à moyens terme ils peuvent devenir particulièrement coûteux.

De manière plus anecdotique, il faut voir qu’une optimisation inutile peut se révéler être contre-efficace. De la même façon qu’un pattern peut se transformer en anti-pattern quand il est mal utilisé, une sur-optimisation peut réduire les performances si elle est mal appliquée ou si elle s’exécute dans un contexte différent de celui qu’on avait en tête quand on l’a codée.

Prenons deux exemples :

  • Ajouter 15% de code en plus, correctement compartimenté, pour multiplier par 25 les performances d’affichage de toutes les pages d’un site web, c’est bien.
  • Ajouter 20% de code en plein milieu de l’existant, pour gagner quelques poussières sur des pages qui représentent moins de 5% du trafic global, c’est inutile.

Mais alors, comment reconnaître une sur-optimisation ?
Posez-vous deux questions simples :

  1. Quel pourcentage de code va être modifié ou ajouté ?
  2. Quel pourcentage d’amélioration puis-je espérer, par rapport au projet global ?

Ensuite il suffit d’appliquer quelques règles :

  • Plus de 33% de code modifié ou ajouté ? Pas bon.
  • Moins de 50% d’amélioration ? Pas bon.
  • Ça prend plus de temps d’optimiser que de coder la fonctionnalité ? Pas bon, sauf si le gain en performance est décuplé (x10 minimum).

Ensuite il ne reste plus qu’à adapter à chaque situation.

Optimiser trop tôt

Ceci est un cas particulier de la sur-optimisation. Comme le disait Donald Knuth :

Premature optimization is the root of all evil.

Les raisons pour lesquelles on peut chercher à optimiser trop tôt sont multiples :

  • L’habitude. On applique une recette telle qu’on l’a déjà appliquée maintes et maintes fois. La force de l’habitude fait qu’on ne réfléchit pas.
  • La volonté de bien faire. Après tout, vouloir optimiser un code pour le rendre plus performant, c’est une bonne chose, non ?
  • La facilité. Quand on a terminé un développement, il est parfois tentant de le peaufiner encore et encore, plutôt que de se pencher sur un autre développement.

N’oubliez pas que ce n’est pas parce qu’on a fait les choses plusieurs fois d’une certaine manière que ça en fait une « bonne pratique » − même si c’était une bonne chose à chaque fois jusque-là.

Mais pourquoi optimiser prématurément est si gênant ?

Tout simplement parce qu’il est plus important de mener un projet à son terme que de l’améliorer. Chaque chose en son temps : développer le projet d’abord, l’affiner ensuite.
À chaque fois qu’un bout de code est complexifié par une optimisation prématurée, on rend plus difficile les ajouts et extensions. Tant qu’un développement n’est pas complet, chaque optimisation qui y est apportée revient à s’ajouter des obstacles dans une course de fond.

Lancement du projet FineDB

Je travaille depuis quelques semaines sur un projet de base de données noSQL, que j’ai nommé FineDB (après mon projet de système de fichiers redondé FineFS, je vous laisse trouver le dénominateur commun).

Plusieurs choses ont amené cette idée à germer dans mon esprit :

  • Comme vous le savez, j’aime le C. À force de ne faire quasiment que du PHP, ça me manquait de travailler sur un bon projet en C. Quelque chose d’assez pointu pour manipuler des concepts assez bas niveau (réseau, threads), et pour lequel les performances soient primordiales − pour justifier l’emploi du C.
  • Il existe plusieurs bibliothèques de fonction permettant de gérer des fichiers de données. La vénérable BerkeleyDB a trouvé des remplaçants de qualité avec LevelDB de Google et LightningDB du projet OpenLDAP.
  • Il existe quelques autres bibliothèques que j’avais envie d’utiliser, comme Snappy (compression ultra-rapide de Google), Nanomsg (communication réseau par le créateur de ZeroMQ), ou encore MessagePack (sérialisation binaire) et TinyCC (compilation à la volée).

À partir de là, les pièces du puzzle se sont assemblées rapidement.

FineDB, c’est quoi

FineDB est une base de données noSQL, utilisant des paires clé/valeur.

FineDB est codé en C et est multi-threads. Les threads communiquent entre eux grâce à Nanomsg. Le moteur de stockage utilise LightningDB. Les données sont compressées à la volée avec Snappy (elles peuvent l’être au niveau du client ou du serveur).

Pour le moment, les fonctionnalités sont très simples : PUT (ajouter ou mettre à jour une clé), GET (récupérer le contenu associé à une clé), DEL (effacer une clé ; en fait un cas particulier du PUT), LIST (lister les clés présentes dans la base).
Il est possible de ne pas spécifier de base, et les commandes concernent alors la base par défaut. Sinon, il est aussi possible de spécifier le nom de la base à laquelle la commande est associée.

La base est toujours dans un état cohérent. Grâce au moteur de stockage utilisé, il est impossible de récupérer des données « en cours d’écriture ».

Le protocole de communication client-serveur se veut le plus efficace possible. C’est un protocole binaire (à la différence des protocoles textuels comme le HTTP), très compact.

FineDB, ça sert à quoi

À la base, FineDB vise la même utilité que Redis, Cassandra, Dynamo, Riak, CouchDB, Couchbase, Project Voldemort, MongoDB et tant d’autres. C’est-à-dire d’offrir un moyen de stocker des données référencées à partir d’un identifiant unique, en étant le plus performant possible.
(oui, je sais que certaines bases que j’ai citées sont orientées document et pas seulement clé/valeur, mais le principe reste valable dans sa globalité)

J’ai fait quelques petits benchmarks, avec des cas d’utilisation réelles (j’ai repris des données que je stocke en production dans un serveur Redis). Comparé à Redis et Couchbase, FineDB est globalement 2 fois plus lent en écriture et 2 fois plus rapide en lecture.
Ces benchmarks m’ont donné envie d’aller plus loin. Être plus lent en écriture s’explique par une meilleure persistance des données, qui sont stockées sur disque et non pas en RAM comme le font les autres (même s’ils ont des stratégies configurables pour stocker les données sur disque au final). Par contre, le fait d’être deux fois plus rapide que Redis en lecture est en soi un exploit intéressant qui peut suffire à légitimer l’existence de FineDB.

Le projet n’en est encore qu’à ses balbutiements. Pour le moment, je me suis attaché à prouver son potentiel. Si mes tests avaient montré des performances désastreuses par rapport aux systèmes déjà existants sur le marché, je ne serais pas allé plus loin.
Maintenant que je sais qu’il y a un intérêt à pousser les choses, j’ai pas mal d’idées à mettre en œuvre, notamment en ce qui concerne les questions de réplication, de redondance et de répartition. L’idée serait d’avoir un système de base de données noSQL qui puisse servir aussi bien sur un petit serveur isolé que pour mettre en place un cluster avec un grand nombre de machines.

Si je schématise, on utilise Redis sur un seul serveur, qu’on va parfois répliquer (en maître-esclave). Par contre, les clusters Redis sont encore instables et fonctionnellement dégradés. À côté de ça, on ne met en place du Hadoop que sur des infrastructures imposantes ; personne n’imagine installer un serveur Hadoop autonome, en dehors d’un cluster servant à du big data.
J’avoue m’intéresser de près à Couchbase, qui fait beaucoup d’efforts pour réunir le meilleur des deux mondes ; il est installable sur un serveur seul, mais il permet aussi de constituer des clusters facilement (avec une interface web absolument bluffante). Mais certains choix de design me feraient réfléchir par deux fois avant de l’utiliser sur mes applications professionnelles, j’imagine ne pas être le seul dans ce cas. Sans oublier que Couchbase n’est pas sous licence libre (même leur « Community Edition »), ce qui me gêne quand même un peu.

Les contraintes

Certaines contraintes de fonctionnement sont imposées par mes choix de design et par ceux des bibliothèques que j’utilise.

Les clés peuvent être longues de 64 KO au maximum.
Les données peuvent faire 4 GO au maximum.
(par comparaison, FoundationDB limite les clés à 10 KO et les données à 100 KO ; sur Amazon DynamoDB, clés et données sont limitées à 64 KO ; dans Couchbase, les clés sont limitées à 250 octets et les données à 20 MO)

Le nom des bases de données est limité à 255 caractères.

La taille des bases de données n’est limitée que par l’espace d’adressage virtuel. Concrètement, ça veut dire que c’est l’espace disque qui détermine la limite, et non pas la RAM disponible. LMDB se débrouille pour mapper les données en RAM pour accélérer leur accès (merci mmap). Par contre, il faut déclarer la taille maximal autorisée ; elle est de 10 MO par défaut (ce qui ne veut pas dire pour autant qu’un fichier de 10 MO est créé dès le départ).

Je ne pense pas que FineDB puisse être porté sous Windows ; trop d’appels POSIX sont employés (peut-être que ça pourrait marcher avec Cygwin mais j’en doute). Pour Mac OS X, ça devrait être possible, mais personnellement je n’ai que du Linux…

Le futur

FineDB évolue très vite en ce moment. J’ai revu certains aspects plusieurs fois, et cela va continuer un bon moment avant de se stabiliser. Je pense que le protocole va encore être modifié en profondeur une ou deux fois avant de prendre sa forme définitive.

Quand l’aspect mono-serveur sera pleinement opérationnel, je vais m’attaquer à la réplication maître-esclave. Une fois qu’elle sera en place, je passerai à la réplication maître-maître.
Si j’arrive à concrétiser les idées que j’ai en tête, je devrais pouvoir proposer quelque chose qui soit relativement simple à mettre en place et à administrer, mais aussi à coder.
Cela servira ensuite de support à un système de cluster, mais on verra ça en temps et heure.

Je réfléchis encore à la possibilité de faire évoluer FineDB vers un système orienté documents. Je me tâte encore beaucoup sur cette question, car cela pourrait faire chuter les performances. D’un autre côté, cet aspect pourrait être optionnel.

Je réfléchis aussi à un moyen d’ajouter des fonctionnalités au cœur du serveur. C’est quelque chose de complexe, mais TinyCC est tellement sexy (j’en ai déjà parlé l’an dernier) que je pourrais l’utiliser pour faire un système de plugins.

Où ça se passe ?

Comme pour mes autres projet, vous pouvez trouver les sources de FineDB sur GitHub : http://github.com/Amaury/FineDB

Comme je l’ai dit plus haut, le projet est encore en phase expérimentale. Je ne cherche pas spécialement de contributeurs, mais n’hésitez pas à me faire part de vos idées.

Ils aiment le C et moi aussi

J’ai déjà écrit plusieurs articles sur les langages de programmation : ceux que je connais, le modèle objet, l’utilisation de TinyCC pour créer un interpréteur, quelques remarques sur les syntaxes, ainsi qu’un article dans lequel j’expliquais les forces du PHP (et qui a reçu des réponses qui me font toujours rire un an après).

J’ai déjà exprimé clairement que mes deux langages préférés sont le PHP et le C ; le premier pour sa souplesse, son adaptabilité et la rapidité avec laquelle on atteint son objectif ; le second pour sa simplicité, son efficacité et le fait qu’il donne un accès direct et transparent à la machine.

Et c’est justement du C dont je vais rapidement vous parler. On a souvent l’impression que c’est devenu avec le temps un langage complètement has-been, et que même ceux qui veulent faire du développement compilé nativement s’orientent tous vers le C++ ; et qu’à moins que vous ne fassiez du développement embarqué (style mbed ou Arduino), choisir le C est forcément un mauvais choix.

J’ai toujours été contre cette vision des choses, mais je me disais que je devais être un peu particulier dans mon genre.

Récemment, je suis tombé sur plusieurs articles sur le web, qui m’ont fait voir que je suis loin d’être le seul à penser que les qualités du C sont toujours intactes.

Popularité

L’index TIOBE est très connu ; il mesure la popularité des langages de programmation. Devinez quel est le langage le plus populaire actuellement ?

TIOBE juin 2013

 

Eh oui, il s’agit bien du C, qui se tire la bourre avec le Java depuis pas mal de temps. Comprenez bien qu’il s’agit d’un indice de popularité, pas d’un benchmark de performances ou une étude sur les qualités intrinsèques des langages ou sur les fonctionnalités qu’ils proposent.

Non, cela montre simplement que le C est toujours un langage très utilisé de par le monde.

Performances

C’est un peu une évidence, mais ça va toujours mieux avec des chiffres. Je connais deux sites qui proposent des benchmarks entre les langages de programmation, basés sur différents algorithmes (certains très théoriques, d’autres plus proches de l’usage réel) : The Computer Language Benchmarks Games et The Great Win32 Computer Language Shootout.

Je vous laisse regarder par vous-même, mais les différents compilateurs C se retrouvent régulièrement en tête − ou dans le peloton de tête − des résultats.

Les autres exemples

Pour tout vous avouer, l’idée d’écrire cet article a commencé à germer en lisant un post sur le blog de Damien Katz, le créateur de CouchDB et directeur technique de CouchBase. Ce post a pour titre The Unreasonable Effectiveness of C.
Dedans, il explique pourquoi il apprécie le C, et pourquoi de plus en plus de parties du CouchBase Server sont écrites en C (il est écrit en Erlang à la base). Son article est très clair, et le point de vue de quelqu’un avec autant d’expérience est forcément très intéressant. Je vous invite à le lire en entier.

Juste pour le plaisir, je vais vous traduire le premier paragraphe :

Pendant des années j’ai essayé de m’éloigner du C. Trop simple, trop de détails à gérer, trop vieux et plein de cochonneries, de trop bas niveau. J’ai eu des amours intenses et torrides avec Java, C++, et Erlang. J’ai construit des choses dont je suis fier avec chacun d’eux, et pourtant ils m’ont tous brisé le cœur. Ils ont fait des promesses qu’ils ne pouvaient pas tenir, créé des cultures qui mettent l’accent sur de mauvaises choses, et ont fait des compromis dévastateurs qui font souffrir douloureusement au final. Et je reviens à chaque fois au C.

Plus récemment, je suis tombé sur un article (en fait deux) écrit par Martin Sústrik, le co-créateur, architecte et principal développeur de ZeroMQ. Je vous ai déjà parlé de cette bibliothèque réseau (, et , ainsi que lors de conférences), c’est un incroyable morceau de logiciel.
Son article s’intitule Why should I have written ZeroMQ in C, not C++. Difficile de faire plus explicite. Là encore, je vous conseille de le lire en entier (ainsi que la seconde partie). C’est assez technique, il parle des exceptions et de la gestion mémoire, notamment. On peut ne pas être d’accord avec sa vision des listes intrusives, mais là encore son expertise rend son opinion intéressante.

Pour info, il a réimplémenté ZeroMQ en pur C. Le projet s’appelle nanomsg, j’ai commencé à l’utiliser, c’est une tuerie.

Et pour moi ?

J’ai commencé tout récemment à travailler sur un nouveau projet (pour occuper mon temps, comme s’il m’en restait de libre…). Si ce projet aboutit, il n’aura d’intérêt que par ses performances. Je me suis donc tourné vers le langage le plus rapide que je connaisse. Et comme à chaque fois que je refais un peu de C, c’est un grand plaisir.

Évidemment, il faut un peu plus de temps que si je codais les choses en PHP (ou un autre langage interprété), particulièrement quand on commence à vouloir manipuler des chaînes de caractères, des listes ou des tableaux associatifs. Et il faut courir après les bibliothèques nécessaires, là où les autres langages fournissent beaucoup de choses de base.
Mais l’impression de puissance est grisante. La mémoire est là, à portée de main, on en fait ce qu’on veut. On contrôle exactement les threads et les processus à notre guise. Pas d’overhead, pas de choses cachées, pas de “magie” qui fait le boulot à notre place. La maîtrise du code est totale, et en contrepartie il n’y a aucune aide derrière laquelle se réfugier.

Avantage secondaire, du code écrit en C compile partout et n’a pas d’autre dépendances que celles que vous avez choisies.

Markdown versus Creole

Je pense que depuis que les hommes peuvent taper du texte sur des ordinateurs, ils ont cherché la manière la plus simple pour mettre ce texte en page, lui appliquer du style et lui donner du sens. De manière générale, cela a conduit à deux grandes approches : celles qui reposent sur des éditeurs de texte qui affichent le résultat final en cours d’édition (les éditeurs WYSIWYG) et celles qui reposent sur les langages de balisage léger.

Je vais m’intéresser à ce deuxième groupe. Pas la peine que je vous fasse un cours. De nos jours, tout le monde a utilisé au moins une fois dans sa vie la syntaxe BBCode (sur n’importe quel forum PHPBB, par exemple) ou une syntaxe de style MediaWiki (sur Wikipedia ou un autre système de wiki).
Avec le temps, on a vu arriver un très grand nombre de langages de ce type. En plus des deux sus-nommés, j’ai en tête ReStructuredText (utilisé dans la documentation Python), AsciiDoc, Textile (utilisé dans les outils de 37signals, par exemple), Markdown (utilisé notoirement par GitHub et StackOverflow), POD (documentation Perl), ou encore Creole.

Je ne compte pas des formats comme Groff, TexinfoLaTeX ou SGML comme des langages de balisages légers. Il suffit d’essayer une fois de faire un vrai document avec l’un de ces systèmes pour se rendre compte qu’il existe plus simple et moderne (mais moins performant − LaTeX est un vrai système de composition de page, particulièrement complet).

Je vais maintenant m’intéresser à deux de ces syntaxes : Markdown et Creole.

Markdown est rapidement devenu le langage de balisage léger le plus hype sur le web. Je n’ai pas vraiment compris pourquoi. Qu’on ne me dise pas que c’est grâce à ses qualités intrinsèques, chacun des langages existant possède des avantages et des inconvénients. Qu’on ne me dise pas que c’est grâce à sa documentation, qui n’est sincèrement pas terrible.

À côté de ça, la genèse de Creole est assez intéressante. Lors du symposium international des wikis en 2006 − et dans le cadre du Wiki Markup Standard Workshop − les créateurs des principales plate-formes de wiki se sont mis autour d’une table pour essayer d’accorder leurs violons au travers d’une syntaxe commune.

Personnellement, j’apprécie beaucoup la syntaxe de Creole. Elle emprunte un très grand nombre d’éléments communs à la plupart des autres langages équivalents. Et pour le reste, elle fait usage de double-symboles qui évitent toute ambiguïté.

Pour illustrer tout ça, je vais comparer les syntaxes de Markdown et de Creole.

PLUS »