Sortie de CLion 2019.3

La dernière version majeure de CLion pour l’année 2019 est sortie. Cet EDI de JetBrains a rapidement su se faire une place sur le marché des environnements de développement C++. Cette version 2019.3 accélère l’autocomplétion de code grâce à Clang, élimine des blocages de l’interface, intègre des outils d’analyse de la couverture du code, permet de lancer du code avec WSL2, améliore le débogueur et gère les concepts de C++20. Cela fait pas mal de choses !

Performance de l’éditeur

Les environnements de développement C++ ne sont pas toujours connus pour leur rapidité, défaut partagé par l’étape de compilation (le C++ est un langage plus lent à compiler que la majorité…). Avec cette version, la solution maison pour gérer l’autocomplétion a été complétée par clangd, un dérivé du compilateur Clang. Cela signifie que, chaque fois que l’EDI lance l’autocomplétion, plusieurs moteurs sont lancés, les résultats étant affichés dès qu’ils sont reçus (avec une limite à cent résultats). clangd est toutefois limité dans ses possibilités, vu qu’il ne peut pas gérer du code en cours d’écriture (il n’est pas compilable, presque par définition).

Pour les projets de taille moyenne, clangd n’apporte pas grand-chose. Par contre, pour des projets bien plus grands, comme LLVM (projet père de Clang) ou Eigen (bibliothèque de calcul numérique), avec une énorme utilisation de fonctionnalités avancées de C++ (comme les templates pour Eigen), le résultat peut être époustouflant, les résultats arrivant jusqu’à plus de cent fois plus vite ! Le processus prenait parfois plusieurs secondes sur un ordinateur portable récent, il est maintenant bien plus raisonnable. Même pour des bibliothèques qui n’utilisent pas tellement de fonctionnalités avancées de C++, comme Qt, les gains sont appréciables.

Code completion: LLVM
Code completion: Eigen
Code completion: Qt

Bon nombre de blocages de l’interface ont été résolus. En particulier, le renommage peut se limiter à des recherches uniquement dans les utilisations du code à renommer (pas les commentaires et les valeurs constantes) : auparavant, la recherche était toujours lancée, même si l’utilisateur n’avait pas besoin de tous les résultats.

refactoring

Les étapes de création et de mise à jour de la base de données des symboles a aussi été énormément retravaillée pour être plus rapide. Selon les projets, cette opération prend de dix à cinquante pour cent de temps en moins, ce qui se ressent très rapidement.

Systèmes de compilation

Jusqu’à présent, CLion ne gérait la compilation qu’à travers les fichiers Makefile générés par CMake. Cette manière de travailler simplifiait fortement la conception de l’éditeur, en analysant directement la sortie de CMake. Désormais, avec CMake 3.15, CLion utilise directement l’API de CMake permettant de récupérer les mêmes données sans devoir passer par un Makefile. Ainsi, CLion peut utiliser n’importe quel générateur géré par CMake pour lancer la compilation, comme Ninja (plus rapide), des solutions Visual Studio ou des projets Xcode (des systèmes plus intégrés pour Windows ou macOS, respectivement). La sélection du système de génération est cependant assez sommaire :

Ninja Gen

Débogage

CLion peut désormais lancer une instance distante de GDB pour le débogage. CLion se charge alors de téléverser l’exécutable à lancer et de le lancer avec gdbserver sur la machine distante.

remote gdb

LLDB 9 est inclus pour Linux et macOS, ce qui a permis de nettoyer une partie du code gérant l’affichage des variables dont le type est un conteneur de la STL (selon l’implémentation utilisée : celle de Clang, libc++, ou celle de GCC, libstdcxx — celle de Clang étant bien mieux gérée).

ubuntu_printers

Concepts de C++20

L’une des nouveautés les plus attendues de C++20 est la notion de concept. L’objectif est de contraindre les possibilités des arguments template avec des prérequis sémantiques (l’argument peut être haché, par exemple). CLion 2019.3 ne se limite pas à la coloration syntaxique, bien évidemment. L’EDI fournit une inspection pour les concepts inutilisés, la complétion automatique pour les types contraints par les concepts std::is_base_of et std::is_same, le renommage, ainsi que la navigation vers la définition et les utilisations d’un concept.

concepts completion

Analyse de code

De nouvelles analyses statiques de code sont disponibles, notamment l’appel de fonctions virtuelles depuis les constructeurs et destructeurs. La vérification orthographique est activée pour les commentaires CMake et Doxygen.

L’analyse de la couverture d’une suite de tests est implémentée et peut utiliser llvm-cov ou gcov.

Source principale : JetBrains. Sources secondaires : 2019.3 EAP: Performance improvements […], 2019.3 EAP: […] CMake, 2019.3 EAP: Debugger improvements, .

Sortie de Julia 1.3

Julia est un langage de programmation cherchant à allier les avantages des langages dynamiques pour l’écriture facile de code et ceux des langages statiques en ce qui concerne la performance. Force est de constater qu’il y arrive raisonnablement bien, mais il manquait jusqu’il y a peu une bonne implémentation de l’exécution sur plusieurs fils d’exécution. Notamment, bon nombre d’objets de base ne géraient pas correctement les appels depuis plusieurs fils d’exécution. Julia 1.3 apporte une première implémentation complète des désirs des développeurs, notamment avec un ordonnanceur de fils d’exécution. Cela permet que chaque bibliothèque prévoie son exécution en parallèle, peu importe la manière dont l’utilisateur l’appellera (en parallèle ou pas) : Julia se charge d’ordonnancer les fils d’exécution pour garantir une performance maximale (plus de détails dans le reste du dossier).

Il reste encore du travail sur la planche au niveau de la performance. Plus précisément, la latence sera un des sujets prioritaires pour Julia 1.4. L’un des problèmes majeurs de Julia est le temps de démarrage de l’environnement, mais aussi la première exécution de certaines fonctions (le cas le plus courant était le paquet Plots.jl). La version de développement actuelle permet déjà de diviser par quatre le temps pour générer le premier graphique (qui passe sous les deux à trois secondes dans la majorité des cas).

Les binaires de Julia 1.3 ne sont pas encore disponibles, mais cela ne devrait pas trop tarder.

Voir le dépôt GitHub de Julia.

Sortie de PyPy 7.2

Python n’est pas un langage de programmation connu pour sa rapidité, mais des implémentations non officielles améliorent fortement la situation. PyPy en fait partie. La différence principale est que PyPy utilise une compilation à la volée (JIT) au lieu d’être un simple interpréteur : plus un bout de code est utilisé, plus PyPy passe de temps à l’optimiser.

La version 7.2 de PyPy vient de sortir. Deux variantes sont disponibles : l’une compatible avec la syntaxe et la bibliothèque standard de Python 2.7.13, l’autre avec Python 3.6.9. La nouveauté de cette version 7.2 est que l’implémentation de Python 3.6.9 n’est plus considérée comme expérimentale. Il reste probablement l’un ou l’autre problème au niveau de la qualité des messages d’erreur générés par des exceptions et par du décodage de chaînes de caractères, mais les développeurs estiment ces problèmes mineurs. Ils planchent désormais sur l’implémentation de Python 3.7, la dernière version de Python actuelle.

PyPy débarque aussi sur les plateformes ARM 64 bits, en collaboration avec ARM et Crossbar.io. En général, l’amélioration de performance obtenue par rapport à l’implémentation de référence de Python est très comparable à celle sur x86_64 : une accélération qui peut monter jusque 44,9 x (par rapport à 58,9 x sur x86_64), mais aussi descendre jusque 0,6 x (c’est-à-dire que PyPy prend presque le double du temps que l’implémentation de référence de Python). Cette amélioration de performance reste préliminaire, vu que beaucoup d’optimisations restent possibles sur cette plateforme, notamment pour exploiter le grand nombre de registres disponibles sur ARM (alors que PyPy est prévu pour les plateformes x86, qui disposent de moins de registres).

Télécharger PyPy 7.2.

Source : PyPy v7.2 released, PyPy JIT for Aarch64.

Les plans des développeurs de Julia pour améliorer le temps d’attente pour le premier graphique

Malgré son caractère haute performance et son utilisation sur des superordinateurs, Julia n’atteint pas toujours ses promesses à ce niveau. Notamment, un problème récurrent est celui du temps nécessaire pour afficher un premier graphique à l’aide du paquet Plots.jl, le plus répandu, mais aussi le plus lent. Importer ce paquet et lancer la première commande de dessin prend énormément de temps, au moins une dizaine de secondes sur des machines puissantes : la situation ne correspond pas au niveau de performance souhaité par les concepteurs du langage.

Après des débats houleux et longs, un chercheur a analysé la source de ce problème de performance : le temps d’inférence, au niveau du compilateur Julia, c’est-à-dire la partie qui cherche à attribuer les types les plus précis possibles aux variables, afin d’accélérer un maximum l’exécution du programme par la suite (bon nombre de gens pensaient, avant, que le problème était surtout dû à LLVM, le compilateur utilisé par après pour générer du code optimisé). Or, vu la structure du paquet, avec un chargement dynamique des moteurs de rendu, il serait très difficile de proposer une nouvelle version de Plots.jl qui ne souffre pas de ce défaut. Certains choix de conception peuvent être améliorés sur ce point de vue. Par exemple, Plots.jl stocke les paramètres dans des dictionnaires, mais avec des types particuliers : alors que Julia est livré avec une série de méthodes pour les dictionnaires précompilées, ce choix de type rend le code précompilé inutile.

Par conséquent, la meilleure chose à faire est d’améliorer l’implémentation de Julia pour résoudre ce problème, surtout qu’il n’est pas limité à Plots.jl : les paquets OrdinaryDiffEq.jl et StochasticDiffEq.jl, utiles pour la résolution d’équations différentielles, rencontrent une variante proche.

Les développeurs de Julia pensent depuis longtemps à inclure un interpréteur dans leur implémentation, de telle sorte que seul le code qui est souvent exécuté est compilé et optimisé (on parle de JIT ou de compilation juste à temps) : la JVM ou encore les moteurs JavaScript de Firefox et de Chrome fonctionnent de la sorte, mais ont fait une transition inverse (passer d’un interpréteur pur à un compilateur à la demande). Un interpréteur écrit en Julia (bien évidemment !) est en cours de développement depuis quelques mois : en fait, JuliaInterpreter.jl est aussi utile pour la conception des moteurs de débogage. Le problème venant de la compilation, le code qui cause ces lenteurs serait d’abord interprété (sans coût a priori) ; ensuite, il serait petit à petit compilé, en fonction des besoins et de la fréquence d’utilisation du code.

Cette solution ne serait pas si facile à implémenter, car un moteur JIT ne peut fonctionner que sur des fonctions : si on remarque, en plein milieu d’une boucle, qu’il faudrait vraiment songer à la compiler, on ne peut le faire qu’avant le prochain appel de la fonction qui contient cette boucle. Si le code est écrit en une seule fonction, il n’y a aucun moyen facile d’optimiser cette boucle. Certes, ce code contrevient aux meilleures pratiques en Julia (écrire de petites fonctions très spécifiques, qui peuvent être facilement optimisées), il n’empêche que tous les utilisateurs ne suivent pas cette règle. Les solutions à mettre en œuvre sont à l’étude depuis février.

En tout état de cause, JuliaInterpreter.jl n’est pas encore apte à prendre en charge une partie aussi critique de l’implémentation du langage, puisque le code n’est pas encore prévu pour la performance : il était d’abord prévu pour du débogage, où la performance n’est pas l’aspect le plus critique.

Ensuite, les développeurs envisagent un deuxième axe d’action, focalisé sur le compilateur. En effet, plus le code passe rapidement par le compilateur (plutôt que par un JIT), plus l’exécution sera rapide. Les solutions envisagées consistent à adapter plus finement certaines heuristiques dans le code ou encore à améliorer les structures de données pour la recherche des méthodes à appeler dans certains cas spécifiques mais courants. Aussi, l’inférence de type se perd parfois dans une longue chaîne d’appels où aucune inférence n’est possible : il est proposé d’utiliser une profondeur maximale pour effectuer cette inférence dans les cas où le compilateur estime probable qu’il ne trouvera jamais de type précis. L’aspect le plus simple pourrait cependant être de stocker les résultats des inférences effectuées lors de la précompilation d’un paquet, ce qui permettrait de n’effectuer ces calculs qu’une seule fois par paquet (et pas une fois par chargement de Julia).

Microsoft libère son implémentation de la STL sous licence Apache 2.0

La conférence CppCon 2019 a vu une annonce de taille de la part de Microsoft, dans la suite de son mouvement de libération du code source : la bibliothèque standard de C++ livrée avec Visual C++ est désormais disponible sous une licence libre, précisément la Apache 2.0 (qui est assez permissive). En fait, Microsoft utilise une variante de cette licence, avec l’extension LLVM, qui permet d’ignorer certaines obligations dans le cas où l’on ne redistribue qu’une version compilée (obligation de donner une copie de la licence, d’indiquer les fichiers qui ont été modifiés, etc.) : libcxx, la bibliothèque standard C++ de LLVM/Clang, utilise déjà cette licence, ce qui devrait faciliter le partage de code entre les projets. Tout le code source de cette bibliothèque est disponible et d’ores et déjà compilable avec Visual C++, mais les tests ne sont pas encore inclus.

Cette implémentation de la bibliothèque standard n’envisage pas d’entrer en compétition avec les implémentations fournies par GCC ou Clang, par exemple : le projet de Microsoft n’envisage pas d’être compatible avec d’autres plateformes que celles de Microsoft. Cependant, les développeurs visent une implémentation de haute qualité : conforme avec les normes et très rapide.

La compatibilité binaire est garantie avec les versions 2015 et 2017 de Visual C++ (cette bibliothèque étant celle fournie avec la version 2019), sauf pour les fonctionnalités qui ont été implémentées avant la finalisation de la norme (working papers et technical specifications du comité de normalisation de C++). La branche WCBF02 (toujours interne à Microsoft) contient des changements incompatibles au niveau binaire, mais pas au niveau des sources (pour une mise à jour de la bibliothèque standard, il faudra donc recompiler ses projets, pas simplement changer de DLL). Cette branche contient une série d’améliorations et de corrections et devrait bientôt être disponible publiquement.

Contrairement à certains projets pourtant décrits comme libres, Microsoft encourage à signaler les défauts et à contribuer du code au projet (dans ce cas, il faudra signer un CLA pour donner à Microsoft les droits nécessaires à la redistribution des contributions, notamment au cas où la licence du projet change, une difficulté à laquelle LLVM s’est confronté pendant plusieurs années).

Dans le futur proche, Microsoft devrait ajouter sa suite de tests interne au projet. La liste des défauts remontés en interne est en cours de migration vers GitHub. Pour le moment, la compilation se passe avec MSBuild, mais une migration vers CMake est en cours. Les fonctionnalités de C++20 sont en cours d’implémentation.

On ne devrait pas voir rapidement d’autres composants de Visual C++ arriver sous licence libre. Microsoft justifie ce choix en indiquant que la bibliothèque standard C++ est assez indépendante du compilateur (contrairement à la bibliothèque standard C, par exemple) et qu’elle évolue très vite par rapport aux autres composants du compilateur.

Source : Open Sourcing MSVC’s STL.

Télécharger le code source.

Sortie de JuMP 0.20

Julia est un langage de programmation orienté performance et calcul scientifique, mais ce faisant il a créé un environnement très productif pour la création de langages spécifiques (DSL) : Julia s’est ainsi créé une niche en optimisation mathématique, JuMP étant une couche de modélisation intégrée au langage de programmation (comme Pyomo en Python), mais avec une performance digne des meilleurs outils (comme AMPL).

La version 0.20 de JuMP vient de sortir, avec une série d’améliorations mineures, comme des corrections de défauts (tous assez mineurs), une documentation améliorée et des messages d’erreur plus précis.

En sus, JuMP dispose de fonctions pour créer un rapport de sensibilité du programme linéaire résolu (en exploitant la solution duale) : lp_objective_perturbation_range permet de récupérer la plage de variation des coefficients de l’objectif telle que la base courante reste optimale, lp_rhs_perturbation_range fait de même pour les membres de droite des contraintes. Ces fonctions ont des limites théoriques : si la base est dégénérée (plusieurs bases pour représenter la même solution), les intervalles indiqués seront bien plus petits qu’attendus. Voir la documentation.

D’autres fonctions font leur apparition, d’utilité plus spécifique. dual_objective_value permet de récupérer la valeur duale de l’objectif (souvent égale à la valeur primale, mais pas toujours, notamment quand l’optimalité n’est pas atteinte). raw_status renvoie le code de retour du solveur directement, sans traduction de la part de JuMP. set_parameter permet de changer les paramètres du solveur. set_objective_coefficient autorise les changements de coefficients de variables dans l’objectif, lors de la résolution d’une séquence de problèmes, avec un impact sur la performance moindre qu’un changement complet de fonction objectif. set_normalized_rhs, normalized_rhs et add_to_function_constant s’occupent de la partie constante des contraintes.

JuMP 0.20 s’appuie également sur la couche d’abstraction des solveurs MathOptInterface 0.9, qui apporte son lot de nouveautés, certaines étant nécessaires aux apports de JuMP 0.20 (comme l’attribut DualObjectiveValue). Les plus anciennes versions de Julia ne sont plus gérées, MOI ne fonctionne qu’avec Julia 1.0 au minimum. La fonction submit a fait son apparition pour soumettre des attributs au solveur, comme des solutions heuristiques ou des contraintes retardées, des notions très utiles pour l’implémentation d’algorithmes d’optimisation avancés et très efficaces (mais pas encore entièrement disponibles dans MOI). La fonction dual_set renvoie l’ensemble dual associé à un domaine, ce qui sert à dualiser efficacement des problèmes d’optimisation (notamment avec Dualization). En outre, l’infrastructure de test a été enrichie pour les dernières nouveautés, elle comporte aussi un module de test de performance des solveurs.

Sources : notes de version de JuMP et MOI.

Julia s’ouvre au parallélisme multifil composable avec sa version 1.3

Lors de la sortie de la première préversion de Julia 1.3, les développeurs se sont montrés fort peu diserts en ce qui concerne les vraies fonctionnalités qui sont arrivés. Les notes de version montraient principalement qu’une très bonne partie de la bibliothèque standard était sûre dans un contexte multifil, mais la vraie bonne nouvelle est que Julia gère désormais le multifil comme prévu dès le début de la conception du langage (l’objet Task était disponible bien avant la première version publique de Julia). Tous les outils nécessaires pour effectuer des calculs à très grande échelle sont donc disponibles : avec plusieurs processus, de manière distribuée sur un réseau, sur des cartes graphiques, mais aussi avec plusieurs fils d’exécution dans un même processus (la documentation n’est pas encore vraiment à jour).

En quoi consiste donc cette nouvelle fonctionnalité ? Julia implémente une forme de parallélisme de tâches : tout morceau de code peut être marqué comme parallélisable ; lors de son exécution, il est lancé dans un autre fil d’exécution. Un ordonnanceur dynamique gère l’exécution de tous ces fils d’exécution. Par exemple, ce morceau de code calcule la suite de Fibonacci de manière récursive et très inefficace, mais surtout sur autant de processeurs que l’on souhaite (si on demande un élément suffisamment éloigné dans la suite) :

import Base.Threads.@spawn

function fib(n::Int)
    if n < 2
        return n
    end
    t = @spawn fib(n - 2)
    return fib(n - 1) + fetch(t)
end

La chose importante à remarquer est @spawn : cette commande permet de lancer un bout de code sur un autre fil d’exécution. Le résultat n’est pas immédiatement retourné, mais bien une référence qui permet de le récupérer une fois qu’il est disponible (à travers fetch).

Derrière, l’ordonnanceur fait en sorte que l’exécution soit la plus efficace possible, peu importe le nombre de tâches qu’on lui demande d’exécuter. Il est prévu pour monter à plusieurs millions de tâches à effectuer, de telle sorte que le programmeur est vraiment libéré de tous les détails pratiques (lancer, synchroniser des fils d’exécution). L’implémentation fonctionne aussi de manière composante : à l’intérieur d’une tâche exécutée en parallèle, on peut appeler des fonctions qui, elles-mêmes, font appel à des tâches parallèles. Toutes ces tâches sont ordonnancées de manière efficace, de telle sorte qu’on ne doive pas se demander comment chaque fonction est codée.

Jusqu’à présent, en Julia, quand on appelait une fonction qui pouvait exploiter une forme de parallélisme à l’intérieur d’une section parallèle, on pouvait très vite remarquer une dégradation de la performance. L’explication est un conflit de ressources (oversubscription) : l’application veut se lancer sur bien plus de cœurs que disponible. Par exemple, sur un ordinateur à quatre cœurs, on divise un gros problème en quatre morceaux équivalents, chacun devant résoudre un gros système linéaire : cette dernière opération peut également se paralléliser de manière efficace ; jusqu’à présent, avec Julia (ou OpenMP, ou encore bon nombre d’autres environnements de programmation parallèles), seize fils d’exécution se chargent de résoudre quatre systèmes linéaires — sur quatre cœurs. Pour améliorer les temps de calcul, il vaut mieux alors désactiver le parallélisme au niveau des systèmes linéaires ! Le nouveau système, entièrement composable, gère ces situations en ne laissant que quatre fils s’exécuter en même temps.

Julia dispose aussi d’une série d’outils bien pratiques : des sémaphores, des verrous, du stockage par fil d’exécution (notamment utilisé pour la génération de nombres aléatoires : chaque fil dispose de son propre objet RNG).

En coulisses, ce système n’est pas d’une simplicité déconcertante à gérer. Les tâches de Julia ne sont pas implémentées de la même manière pour chaque système d’exploitation : Windows dispose de fibres, conceptuellement très proches, mais pas Linux (certains bibliothèques implémentent ce genre de mécanisme, cependant). Chaque tâche nécessite sa propre pile d’appels de fonction pour gérer l’exécution, mais elle doit être gérée de manière particulière : le système d’exploitation se charge d’énormément de choses pour passer l’exécution d’un fil à l’autre, mais, en ce qui concerne les tâches, les applications doivent implémenter beaucoup de choses elles-mêmes. Pour le moment, Julia considère qu’une tâche ne peut s’exécuter que dans un seul fil d’exécution, sans possibilité de passer sur un autre fil pour mieux exploiter le processeur, mais cela arrivera dans le futur.

L’implémentation actuelle est complète d’un point de vue Julia, mais a une limitation majeure : les bibliothèques appelées par Julia (par exemple, pour la résolution de systèmes linéaires) sont écrites en C et ne gèrent pas nativement cette forme de parallélisme. Ce sera corrigé dans une version à venir. L’API n’est pas encore définitive et pourrait légèrement évoluer avant d’être entièrement stabilisée. La gestion des entrées-sorties n’est pas encore au point : un verrou global est imposé sur ces opérations, mais cela devrait changer. Une fois ces éléments améliorés, la bibliothèque standard pourrait commencer à exploiter le parallélisme de tâches de manière interne, par exemple pour trier des tableaux suffisamment grands.

Source : Announcing composable multi-threaded parallelism in Julia.

Sortie de Julia 1.3 Alpha 1

Julia est un langage de programmation qui cherche à apporter la facilité d’utilisation des langages dynamiques (comme Python ou Ruby) à la performance de langages comme C, C++ ou Fortran. Cela semble marcher, vu que le langage gagne des places dans des classements comme TIOBE ou en comptant le nombre d’étoiles du dépôt sur GitHub.

La version 1.2 n’est pas encore finalisée (on en est actuellement à la RC 2), mais la 1.3 commence d’ores et déjà à pointer le bout de son nez. Les nouveautés syntaxiques sont très limitées : le nouveau caractère d’Unicode 12.1 est utilisable (il représente la nouvelle dynastie japonaise, suite à l’abdication de l’empereur Akihito) ; plus important, on peut ajouter des méthodes à des types abstraits. Ainsi, on peut rendre un type abstrait appelable comme une fonction (auparavant, on ne pouvait le faire que sur des types concrets).

Au niveau du parallélisme, la bibliothèque standard fait de gros progrès. Toutes les opérations d’entrée-sortie sont sûres en contexte multifil, que ce soit sur des fichiers ou des sockets réseau, à l’exception des opérations d’entrée-sortie effectuées en mémoire (comme IOBuffer, même si BufferStream est sûr). La même propriété a été apportée au générateur global de nombres aléatoires. La macro @spawn peut s’utiliser pour exécuter une tâche sur un fil d’exécution disponible, même si elle est marquée comme expérimentale.

Source : notes de versions.

Firefox pourra bientôt exécuter du code Julia

WebAssembly est, de certains points de vue, une révolution pour les navigateurs : il est possible d’écrire du code de très haute performance, à un très bas niveau d’abstraction, pour que le navigateur l’exécute. Autant cela n’est pas nécessaire pour une petite animation, autant le système est pleinement exploité quand cette animation est calculée par un script Python exécuté à l’intérieur du navigateur. C’est l’objectif de Pyodide : exécuter du code Python dans un navigateur, avec une performance comparable à l’interpréteur Python classique.

Le projet Iodide englobe notamment Pyodide et cherche à faciliter l’interprétation de code dans un certain nombre de langages à l’intérieur d’un navigateur, notamment pour faciliter l’utilisation de code “intensif” en calcul, notamment pour effectuer de l’apprentissage automatique au sein de son navigateur. Le projet définit quatre niveaux de finalisation pour une extension : capable d’échanger des chaînes de caractères (1), des types de données simples (2), des classes (3) et des tableaux multidimensionnels (4). Seul Python arrive presque au niveau 4.

Dans le cadre d’Iodide, Mozilla a décidé de financer un développeur, Valentin Churavy, hébergé au MIT pour y effectuer sa thèse de doctorat, pour travailler sur Julide, dont l’objectif est d’apporter Julia aux navigateurs Web. Julia est un langage conçu, dès ses prémisses, pour apporter une très bonne performance. Ce financement ne court que pour les six premiers mois de 2019, ce qui devrait être suffisant pour proposer un prototype viable.

Source : Mozilla is funding a way to support Julia in Firefox.

Gen, un langage probabiliste universel dans Julia

La programmation probabiliste est un paradigme de programmation assez peu répandu, notamment parce que ses utilisations sont liées à l’intelligence artificielle (et pas à grand-chose d’autre). L’objectif est d’effectuer des calculs sur des variables aléatoires (c’est-à-dire une quantité qui n’est pas connue exactement), puis d’effectuer de l’inférence pour expliquer des données. Par exemple, pour de la reconstruction de pose 3D (trouver l’orientation des bras, des torses, des jambes, etc. à partir d’une photo d’une personne), on dispose d’une image à expliquer. Une manière classique d’aborder le problème serait de trouver les bras, le torse, les jambes, puis leurs orientations. En programmation probabiliste, on préférera utiliser une série de variables aléatoires (orientation des bras, des jambes, etc.) : une fois les valeurs fixées, on peut calculer exactement la pose de la personne. Ensuite, on peut comparer l’image obtenue avec celle d’entrée : si elles sont très proches, on a trouvé les bonnes valeurs des variables aléatoires. Le processus d’inférence consiste, ici, à tirer un grand nombre de valeurs différentes pour les variables aléatoires, puis de prendre la meilleure correspondance avec l’image d’entrée, la “meilleure explication”.

Gen est un système prévu pour la programmation probabiliste “généraliste”, sans a priori sur les variables aléatoires que l’on peut utiliser. Ce système est embarqué comme une bibliothèque pour le langage Julia.

En pratique, pour effectuer de l’inférence, il faut deux choses : d’un côté, une fonction génératrice, qui tire au hasard (ou choisit de manière plus intelligente) des valeurs pour les variables aléatoires et détermine d’autres valeurs (une sorte de résultat final : par exemple, une pose) ; de l’autre, des données à expliquer.

Par exemple, on dispose d’une série de coordonnées de points et on cherche une droite qui passe par ces points (on en connaît les coordonnées : x_i et y_i). Au lieu d’utiliser une régression linéaire, on peut passer par de la programmation probabiliste. Une droite, en deux dimensions, est déterminée par deux paramètres (une pente et une intersection avec l’un des axes, par exemple). Pour comparer le modèle aléatoire aux données, il faut calculer les coordonnées de points : on connaît les x_i, on détermine les y_i selon les paramètres tirés aléatoirement ; après, on comparera ces y_i aux vraies valeurs. La fonction génératrice ressemble alors à une fonction Julia normale, mais annotée avec @gen (de même, chaque variable aléatoire est annotée avec @trace) :

@gen function line_model(xs::Vector{Float64})
    slope = @trace(normal(0, 1), :slope)
    intercept = @trace(normal(0, 2), :intercept)
    
    for (i, x) in enumerate(xs)
        @trace(normal(slope * x + intercept, 0.1), (:y, i))
    end
    
    return length(xs)
end

Ensuite, on peut effectuer de l’inférence (ici, en tirant au hasard des valeurs cent fois — un procédé très cru, pas forcément rapide, mais facile à implémenter et comprendre !) :

xs = [-5., -4., -3., -.2, -1., 0., 1., 2., 3., 4., 5.]
ys = [6.75003, 6.1568, 4.26414, 1.84894, 3.09686, 1.94026, 1.36411, -0.83959, -0.976, -1.93363, -2.91303]

observations = Gen.choicemap()
for (i, y) in enumerate(ys)
    observations[(:y, i)] = y
end

(trace, _) = Gen.importance_resampling(model, (xs,), observations, 100)

En répétant l’expérience un certain nombre de fois, on trouve une série de droites qui expliquent au mieux, pour chaque centaine de tentatives, les données :

Après cette inférence, Gen a appris la densité la plus probable des paramètres de la ligne : il peut ensuite générer de nouveaux points, grâce à ce qu’il a enregistré, en utilisant la fonction Gen.generate.

La différence entre Gen et d’autres systèmes de programmation probabiliste est sa nature universelle : le système peut être utilisé pour n’importe quelle tâche de programmation probabiliste (même si cet article se focalise sur l’inférence a posteriori). L’implémentation est prévue pour être facile à adapter à ses propres besoins (on peut écrire son propre moteur d’inférence, même s’il en existe déjà un certain nombre). Jusqu’à présent, les systèmes probabilistes disposaient soit de bonnes capacités d’inférence (mais une modélisation limitée), soit au contraire étaient aussi généralistes, mais sans algorithme d’inférence performant. Gen renverse la donne, en étant universel, mais aussi en incluant une grande variété de possibilités d’inférence.

Source : Gen: a general-purpose probabilistic programming system with programmable inference. L’exemple développé provient de la documentation de Gen.

Voir aussi : le code source de Gen.