Présentation de Julia v1.10, la nouvelle version du langage de programmation apporte des améliorations en termes de rapidité et de confort pour les développeurs

Voici la présentation de la nouvelle version de Julia : Julia v1.10. La version 1.10 apporte d'importantes améliorations en ce qui concerne la précompilation du code et les temps de chargement. Elle comprend également un nouvel analyseur syntaxique écrit en Julia.

Julia est un langage de programmation polyvalent et open-source axé sur le calcul scientifique à haute performance. Parmi les caractéristiques inhabituelles de Julia, on peut citer la métaprogrammation inspirée de Lisp, la possibilité d'examiner les représentations compilées du code dans le REPL ou dans un "reactive notebook", un système de types et de répartition avancé, ainsi qu'un gestionnaire de paquets sophistiqué et intégré. La version 1.10 apporte d'importantes améliorations en termes de rapidité et de confort pour les développeurs, notamment en ce qui concerne la précompilation du code et les temps de chargement. Elle comprend également un nouvel analyseur syntaxique écrit en Julia.

Nom : 1.png
Affichages : 20186
Taille : 12,9 Ko

Temps de chargement

Le temps nécessaire pour importer des paquets a été un problème courant depuis la première version publique de Julia. Ce temps est parfois appelé "time to first plot", ou, plus généralement, "time to first x", où "x" est la sortie d'une fonction lorsqu'elle est appelée pour la première fois à partir d'un package fraîchement chargé. Presque toutes les versions majeures de Julia ont amélioré les temps de compilation et de chargement.

La dernière version réduit ces délais au point que la stratégie consistant à créer des images système personnalisées avec des paquets préchargés n'est plus nécessaire. Cette stratégie utilisait le module PackageCompiler pour créer un binaire Julia personnalisé avec les paquets couramment utilisés, afin qu'il puisse démarrer instantanément, prêt à tracer ou à effectuer d'autres tâches sans délai.

Voici un exemple d'un test, où un simple "using Plots ; plot(sin)" a été chronométré en utilisant Julia v1.10 sur un ordinateur portable de faible puissance. Il a rapporté un temps de 2,16 secondes ; dans ce temps, il a fallu charger le runtime Julia, importer le paquetage Plots, et créer un simple tracé. Noter que cette expérience n'utilise pas une nouvelle installation de Julia. Plots a déjà été téléchargé et précompilé (avec ses dépendances), ce qui peut prendre un certain temps, mais c'est un coût unique. Pour avoir une idée des améliorations apportées par la version 1.10, le testeur a répété la même commande chronométrée avec deux versions précédentes de Julia installées. Julia v1.9 prend 3,6 secondes et v1.8 18,1 secondes.

Les développeurs de Julia ont utilisé plusieurs stratégies pour parvenir à cette dernière amélioration du chargement du code. La version majeure précédente a introduit la mise en cache du code natif et les extensions de paquets pour les dépendances faibles, ainsi que la possibilité pour les auteurs de paquets de livrer un code binaire natif précompilé avec leur logiciel. Une partie de l'amélioration récente du temps de chargement est due au fait que les développeurs de paquets tirent davantage parti de ces deux mécanismes ; au fur et à mesure que ces pratiques se répandront dans la communauté, on verra d'autres réductions des temps de latence, même en l'absence d'améliorations apportées à Julia elle-même.

La dernière version corrige un oubli dans la génération des caches de code : auparavant, si un utilisateur compilait du code avec plus d'une instance de Julia (une situation qui peut survenir dans des environnements informatiques parallèles), une condition de course pouvait s'ensuivre, avec différents processus Julia écrivant simultanément dans les mêmes fichiers de cache. Ce problème a été éliminé grâce à l'utilisation de fichiers de verrouillage.

Une partie de la réduction du temps de chargement des paquets est le résultat de la parallélisation de l'étape initiale de précompilation. La précompilation qui se produit lors de l'installation des paquets se fait en parallèle depuis longtemps, mais l'étape ultérieure, lorsque l'utilisateur importe des fonctions dans un programme ou un environnement, était un processus sériel jusqu'à la version 1.10. C'est cette étape de précompilation qui affecte la réactivité dans l'utilisation quotidienne, donc l'accélérer a un impact plus important lorsque l'on travaille dans le REPL ou que l'on exécute des programmes.

Julia est livrée avec une version de LLVM contenant des patches pour corriger des bugs, principalement dans les méthodes numériques. La mise à jour vers LLVM 15 qui a été fournie avec la version 1.10 a également eu un impact sur la réactivité de Julia, car la nouvelle version du compilateur a de meilleures performances.

Pour les utilisateurs qui souhaitent garder un œil sur les temps de précompilation des paquets dans la REPL, la commande Pkg.precompile(timing=true) précompilera tous les paquets de votre environnement qui le nécessitent, et fournira un rapport par paquet sur les temps de compilation.

Langage, bibliothèques et durée d'exécution

En plus d'une précompilation plus rapide, la version 1.10 apporte une amélioration des performances d'exécution et quelques commodités pour les développeurs.

L'analyseur Julia a, depuis la v1.0 jusqu'à maintenant, été implémenté dans Femtolisp. Ce dialecte Scheme était, selon les mots de son créateur, Jeff Bezanson (un des inventeurs de Julia), dans son README, "une tentative d'écrire l'interpréteur lisp le plus rapide possible en moins de 1000 lignes de C".

Avec la version 1.10, l'analyseur Femtolisp a été remplacé par un analyseur écrit en Julia. Le nouvel analyseur présente trois avantages : il est plus rapide, il produit des messages d'erreur syntaxique plus utiles, et il fournit une meilleure correspondance entre le code source et le code compilé, qui associe des emplacements dans le code compilé à leurs lignes correspondantes dans le code source. Cette dernière amélioration se traduit également par de meilleurs messages d'erreur et permet d'écrire des débogueurs et des linters plus sophistiqués.

La figure suivante illustre l'amélioration des rapports d'erreur dans la version 1.10 :

Nom : 2.png
Affichages : 6581
Taille : 23,5 Ko

Comme on peut le voir, la position exacte de l'erreur est indiquée. La même erreur commise avec la v1.9 produit un message un peu moins sympathique, mentionnant l'espace supplémentaire mais n'indiquant pas visuellement son emplacement.

Femtolisp effectue toujours l'abaissement du code pour la langue. Dans la dernière version, comme dans les versions précédentes, démarrer une session interactive avec l'option --lisp permet d'accéder au REPL de Femtolisp.

Les traces de pile ont également été rendues plus concises et utiles en omettant les informations inutiles, ce qui répond à une plainte fréquente des programmeurs de Julia.

Julia a toujours utilisé un garbage collector de traçage. Les programmeurs d'applications n'ont jamais eu besoin de savoir quoi que ce soit à ce sujet, bien que, en tant qu'optimisation, il est souvent possible d'éviter complètement le ramasse-miettes en prenant soin de ne pas allouer de mémoire dynamiquement. Néanmoins, pour certains algorithmes, ces stratégies ne fonctionnent pas.

Les performances d'exécution de la version 1.10 ont été améliorées en parallélisant la phase de marquage du ramasse-miettes, ce qui a permis d'obtenir une accélération presque linéaire en fonction du nombre de threads alloués au ramasse-miettes. Julia peut être démarré avec n'importe quel nombre de threads de travail, défini avec l'option -t. Le nombre de threads utilisés pour le ramasse-mietteS est la moitié du nombre de threads de travailleur, par défaut, mais l'utilisateur peut le définir avec le nouveau drapeau --gcthreads.

L'utilisation des caractères Unicode a fait l'objet de quelques ajustements et raffinements, comme c'est le cas dans toutes les versions de Julia. Il y a deux nouvelles flèches fantaisistes (⥺ et ⥷) qui peuvent être utilisées comme noms pour les opérateurs binaires. Les physiciens seront soulagés d'apprendre que deux glyphes similaires pour hbar sont maintenant traités comme identiques. Le glyphe ∜ calcule la racine quatrième, sans surprise, en utilisant une nouvelle fonction de Base.math appelée fourthroot(). Son implémentation a été motivée par le désir de ne pas "gaspiller un symbole Unicode parfaitement bon", et souligne également que [C]fourthroot()[/CB] est plus rapide que ce que certains programmeurs pourraient utiliser : x^(1/4).

En ce qui concerne les nouvelles fonctions, la nouvelle version en contient une bonne poignée. La fonction tanpi(x) calcule tan() avec plus de précision pour les arguments de grande taille. Par exemple, en notant que tan(π/4) = 1 exactement, et que la tangente a une période de π :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
 
    julia> tanpi(1/4 + 2e10)
    0.9999999999999999
 
    julia> tan((1/4 + 2e10))
    0.999995497014194
Quelques nouvelles fonctions liées à la mémoire, memmove(), memset() et memcpy(), ont été ajoutées à la bibliothèque C standard de Julia, pour faciliter l'interaction avec les routines de la bibliothèque C.

La fonction de Base pour le calcul des coefficients binomiaux, binomial(x, n) (x choisie n), accepte maintenant des x non entiers, en appliquant la définition standard pour les coefficients binomiaux étendus ; n doit toujours être un entier.

Julia dispose d'une fonction printstyled() qui écrit des messages à l'écran avec des effets optionnels tels que les couleurs, le soulignement, la vidéo inversée, et d'autres ; le fait que l'utilisateur voit les effets dépend des capacités de l'émulateur de terminal utilisé. Julia v1.10 ajoute une option italique.

view dans Julia est une section d'un tableau (appelé parent), avec lequel elle partage la mémoire. La fonction parent() renvoie le tableau parent de view. SubString applique la même idée aux chaînes de caractères : c'est un morceau d'une chaîne parentale qui ne crée pas de nouvel objet. Auparavant, parent() ne fonctionnait qu'avec les tableaux de views. La fonction a été étendue pour fonctionner avec les types SubString :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
 
    julia> s = "abcdefgh"
    "abcdefgh"
 
    julia> ss = SubString(s, 6:8)
    "fgh"
 
    julia> ss[1]
    'f': ASCII/Unicode U+0066 (category Ll: Letter, lowercase)
 
    julia> parent(ss)
    "abcdefgh"
La fonction startswith() détermine si une chaîne commence par un caractère ou une sous-chaîne donnée. Elle a été étendue aux flux d'E/S (par exemple, les fichiers). Elle examine le flux non pas depuis le début, mais depuis la position de lecture actuelle, comme le montre cet exemple :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
 
    julia> maybePNG = open("fig1.png")
    IOStream(<file fig1.png>)
 
    julia> seek(maybePNG, 1);
 
    julia> startswith(maybePNG, "PNG")
    true
 
    julia> position(maybePNG)
    1
Cet exemple vérifie la présence du nombre magique à un octet du fichier pour déterminer s'il s'agit d'une image PNG. La fonction startswith() jette un coup d'œil à l'extrémité requise du flux sans modifier la position de lecture, comme le montre la dernière ligne de l'exemple.

Dans Julia, les nombres rationnels sont définis et affichés avec une double barre oblique, comme 3//4. Dans la dernière version, lors de l'impression d'un tableau, les nombres rationnels sont affichés comme des entiers s'ils ont une valeur intégrale (s'ils peuvent être réduits à avoir un dénominateur de un). Voici un exemple :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 
    julia> TM = Matrix{Rational}(undef, 5, 5);
 
    julia> for i ∈ 1:5, j ∈ 1:5
              if j<i
                  TM[i, j] = i//j
              else
                  TM[i, j] = 0//1
              end
           end
 
    julia> TM
    5×5 Matrix{Rational}:
    0   0     0     0    0
    2   0     0     0    0
    3  3//2   0     0    0
    4   2    4//3   0    0
    5  5//2  5//3  5//4  0
Ceci construit une matrice triangulaire inférieure dont la structure est facile à voir d'un coup d'œil. Voici comment les versions précédentes de Julia affichent le même tableau :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
 
    0//1  0//1  0//1  0//1  0//1
    2//1  0//1  0//1  0//1  0//1
    3//1  3//2  0//1  0//1  0//1
    4//1  2//1  4//3  0//1  0//1
    5//1  5//2  5//3  5//4  0//1
Ce style d'affichage des nombres rationnels ne s'applique qu'aux membres des tableaux ; en dehors de ce contexte, il n'y a pas de changement.

La nouvelle version apporte quelques améliorations techniques à la bibliothèque d'algèbre linéaire, qui fait partie de la bibliothèque standard. L'une des nouveautés est la fonction hermitianpart(), qui calcule efficacement la partie hermitienne d'une matrice carrée.

Création de binaires

La façon normale d'utiliser Julia est de télécharger et d'installer le runtime et de travailler soit dans un environnement interactif, tel que le REPL ou un notebook Pluto, soit d'exécuter des fichiers de programme à partir de la ligne de commande, en utilisant la commande shell julia. Dans les deux cas, le compilateur est disponible pour générer un nouveau code machine natif chaque fois qu'il rencontre un appel de fonction avec une combinaison de types d'arguments qui n'a pas été compilée auparavant. Cela permet d'utiliser pleinement la répartition multiple et les types définis par l'utilisateur sans sacrifier les performances, à l'exception des temps de compilation nécessaires lorsque le code d'une nouvelle méthode doit être généré.

Il existe cependant d'autres scénarios d'utilisation qui ne sont pas satisfaits par les mécanismes standard. On peut vouloir éliminer autant que possible les délais de compilation, par exemple si un programme Julia est appelé de manière répétée dans un script shell. Même les petites pénalités de compilation encore présentes dans la v1.10 pourraient être indésirables. On peut aussi vouloir donner notre programme à quelqu'un qui, pour une raison étrange, n'a pas installé le runtime Julia. Dans ce cas, cette personne aura besoin d'une version binaire autonome de notre programme.

Il existe deux packages principaux qui fournissent des utilitaires pour générer différents types de programmes Julia compilés. StaticCompiler est un paquetage qui peut créer de petits binaires compilés statiquement à partir de sources écrites dans un sous-ensemble sévèrement limité de Julia. Il s'agit d'un paquetage expérimental destiné aux programmeurs aventureux. StaticCompiler est la base des expériences en cours pour compiler Julia en WebAssembly afin de l'exécuter dans les navigateurs web. L'année dernière, des progrès significatifs ont été réalisés dans ce domaine.

Un outil plus généralement utile est le package PackageCompiler. Il peut compiler des programmes écrits en Julia sans contrainte en images de système ou en binaires autonomes distribuables. Le premier objectif est utilisé afin d'avoir un environnement Julia avec un ensemble de paquets intégrés qui démarre instantanément. Ceci était très utile dans les versions précédentes de Julia, lorsque les temps de précompilation interféraient avec l'utilisation des programmes Julia en tant qu'utilitaires de routine (un script qui prenait une demi-minute pour tracer un petit morceau de données n'était pas pratique). Maintenant que les temps de précompilation et de chargement sont beaucoup plus courts, cette utilisation de PackageCompiler est presque obsolète.

Mais il reste l'utilitaire standard pour créer des binaires à distribuer ou à utiliser dans des environnements où le runtime Julia n'est pas installé. Bien que toujours en développement actif, c'est un outil mature. Jusqu'à récemment, la principale plainte était la taille énorme des programmes compilés : PackageCompiler transformait un programme "hello world" en une monstruosité de 900 Mo. Ceci est dû au fait que tout a été inclus, non seulement l'ensemble du runtime Julia, mais aussi tout ce qui se trouve dans la bibliothèque standard. Par exemple, les routines BLAS étaient incluses même si le programme ne faisait pas d'algèbre linéaire.

Au cours de l'année écoulée, la taille d'un programme "hello world" compilé a été ramenée à 50 Mo, ce qui rend l'approche de PackageCompiler beaucoup plus pratique. Ces progrès résultent de l'élimination des parties inutiles du moteur d'exécution et des bibliothèques inutilisées. Ce travail est en cours et fait l'objet d'une attention particulière de la part des développeurs.

Ressources pédagogiques

Le manuel officiel de Julia est complet et précis, mais son organisation peut rendre difficile la recherche de ce dont vous avez besoin, surtout si vous êtes débutant. Heureusement, Julia est utilisée depuis suffisamment longtemps pour qu'une constellation d'articles et de livres se soit développée autour d'elle, écrits par des auteurs issus d'une grande variété de milieux et destinés à des lecteurs allant du programmeur débutant au scientifique expérimenté en informatique. Plusieurs livres ont récemment été publiés par des auteurs ; par exemple, Bogumił Kamiński, Erik Engheim, et Logan Kilpatrick. Leurs livres sont rassemblés dans une liste maintenue sur le site officiel de Julia.

Une autre bonne ressource pour en savoir plus sur Julia et les projets qui en tirent profit sont les conférences de la JuliaCon sur YouTube.

Enfin, lorsque vous explorerez sérieusement le langage et l'écosystème, la plateforme de discussion Julia Discourse deviendra une ressource précieuse. Les développeurs de Julia, les auteurs de paquets et les utilisateurs expérimentés y participent activement ; la communauté est accueillante, patiente et serviable.

Conclusion

Entre les améliorations de la précompilation et des temps de chargement, et les progrès dans la création de petits binaires, deux plaintes majeures et constantes, des débutants comme des utilisateurs chevronnés de Julia, ont été prises en compte. Le travail sur ces deux domaines continue et il est probable qu'il y ait plus d'améliorations dans un futur proche, en particulier dans le domaine de la génération de binaires. StaticCompiler et les outils WebAssembly associés faciliteront l'écriture d'applications web en Julia pour une exécution directe dans le navigateur ; c'est déjà possible, mais cela pourrait devenir plus pratique au cours des prochaines années.

Source : Présentation Julia v1.10

Et vous ?

Quel est votre avis sur le sujet ?

Voir aussi :

La version 1.10 de Julia est disponible, avec de nouvelles fonctionnalités, dont des améliorations de performance et des changements de comportement marginaux et non perturbateurs

Adoption du langage de programmation Julia : Logan Kilpatrick, défenseur de la communauté des développeurs Julia, livre son analyse, dans un billet de blog

Le potentiel du langage de programmation Julia pour le calcul en physique des hautes énergies, un domaine qui nécessite d'énormes quantités de calcul et de stockage, selon une étude scientifique