La dernière version de correction de bogues mineurs, 1.7, a été publiée le 2 Decembre. Cette version corrigeait les erreurs de synchronisation, affinait le support de l'ordonnancement des charges de travail sur plusieurs threads, avec le générateur de nombres aléatoires par défaut plus convivial pour les threads, et ajoutait les atomiques comme caractéristique du langage primitif.
Julia est un langage de programmation open source et un écosystème pour le calcul scientifique de haute performance. Conçu par des chercheurs du MIT en 2009 et dévoilé pour la première fois au grand public en 2012, il est doté d’une syntaxe familière aux utilisateurs d'autres environnements de développement similaires. Julia connaît une croissance fulgurante depuis sa sortie et certains vont même jusqu’à dire qu’il s’agit du langage idéal pour le calcul scientifique, la science des données et les projets de recherche. Le langage s'est popularisé lorsque le projet est devenu open source en 2012. Il est actuellement disponible sous licence MIT.
À la base, ses concepteurs voulaient un langage avec une licence libre et renfermant de nombreux avantages surtout pour la communauté scientifique. « Nous voulons un langage open source, avec une licence libre. Nous voulons un langage qui associe la rapidité de C et le dynamisme de Ruby. En fait, nous voulons un langage homoïconique, avec de vraies macros comme Lisp et avec une notation mathématique évidente et familière comme MATLAB. Nous voulons quelque chose d’aussi utilisable pour la programmation générale que Python, aussi facile pour les statistiques que R, aussi naturel pour la gestion de chaîne de caractères que Perl, aussi puissant pour l’algèbre linéaire que Matlab et aussi bien pour lier des programmes que le shell. Nous voulons qu’il soit à la fois interactif et compilé », avaient-ils déclaré en 2012.
La plupart des versions de Julia sont programmées dans le temps et ne sont donc pas planifiées autour de fonctionnalités spécifiques. Voici, ci-dessous, quelques améliorations apportées à Julia dans la version 1.8 :
const sur les champs dans les structs mutables
Julia supporte maintenant l'annotation de champs individuels d'une structure mutable avec des annotations const :
Code : | Sélectionner tout |
1 2 3 4 | mutable struct T x::Int const y::Float64 end |
qui fait que le champ y est constant (et ne peut donc pas être réaffecté après la création du type). Cela peut être utilisé pour faire respecter des invariants, mais le compilateur peut également en tirer parti pour améliorer le code généré.
Site d'appel @inline
Avant Julia 1.8, la macro @inline ne pouvait être utilisée que sur les définitions de méthodes et la fonction était inlined à tous les sites d'appel de cette méthode. Cependant, il peut être utile de faire le choix pour un site d'appel donné si une fonction doit être inlined. C'est pourquoi il est désormais possible d'appliquer la macro @inline à un site d'appel donné, comme @inline f(x), qui indiquera au compilateur d'inclure la méthode à cet appel spécifique.
Les variables globales non constantes dans Julia ont une pénalité de performance parce que le compilateur ne peut pas raisonner sur leur type puisqu'elles peuvent être réassignées à un autre objet d'un autre type pendant l'exécution. Dans Julia 1.8, il est maintenant possible de spécifier le type d'une variable globale non constante en utilisant la syntaxe x::T où T est le type de la variable globale. Essayer de réassigner la variable à un objet d'un autre type provoque des erreurs :
Code : | Sélectionner tout |
1 2 3 4 5 6 | julia> x::Int = 0 0 julia> x = "string" ERROR: MethodError: Cannot `convert` an object of type String to an object of type Int64 ... |
L'annotation de type des variables globales supprime une grande partie (mais pas la totalité) du coût de l'utilisation de variables globales non constantes.
Nouveau planificateur par défaut pour @threads
Julia a eu la macro @threads pour paralléliser une boucle for même avant que le runtime générique de tâches parallèles soit introduit dans Julia 1.3. En raison de cette raison historique, @threads a fourni un ordonnancement statique pour éviter de casser les programmes qui s'appuient accidentellement sur ce comportement strict (voir :static scheduling dans la documentation). Ainsi, pour travailler agréablement avec le reste du système multitâche, l'ordonnanceur :static a été introduit dans Julia 1.5 pour aider les gens à se préparer à changer le comportement d'ordonnancement par défaut dans le futur, c'est-à-dire maintenant. Dans Julia 1.8, les programmes écrits avec @threads peuvent pleinement exploiter l'ordonnanceur dynamique et composable.
Pour illustrer l'amélioration, considérez le programme suivant qui simule une charge de travail intensive pour le CPU et qui nécessite quelques secondes pour se terminer :
Code : | Sélectionner tout |
1 2 3 4 5 | julia> function busywait(seconds) tstart = time_ns() while (time_ns() - tstart) / 1e9 < seconds end end; |
Avant Julia 1.8, @threads utilise toujours tous les threads des travailleurs. En tant que tel, @threads ne se termine pas avant que toutes les tâches précédentes soient terminées :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 | julia> @time begin Threads.@spawn busywait(5) Threads.@threads for i in 1:Threads.nthreads() busywait(1) end end 6.003001 seconds (16.33 k allocations: 899.255 KiB, 0.25% compilation time) |
Le temps d'exécution est d'environ 6 secondes. Cela signifie qu'une tâche créée en interne pour @threads attend que la tâche busywait(5) soit terminée. Cependant, dans Julia 1.8, nous avons :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 | julia> @time begin Threads.@spawn busywait(5) Threads.@threads for i in 1:Threads.nthreads() busywait(1) end end 2.012056 seconds (16.05 k allocations: 883.919 KiB, 0.66% compilation time) |
Cela prend 2 secondes car l'un des threads non occupés peut exécuter deux des itérations d'une seconde pour terminer la boucle for.
Profiler
Nouveau profileur d'allocation
Les allocations inutiles dans le tas peuvent sérieusement dégrader les performances, et les outils existants pour les traquer (à savoir @time et --track-allocation) ne fournissaient pas tout à fait les détails fins, la bonne visualisation et la facilité d'utilisation que nous recherchions. Nous avons donc créé le profileur d'allocation ([C=Julia]Profile.Allocs), qui capture les allocations du tas avec le type, la taille et la trace de la pile de chacune d'entre elles, de sorte qu'elles peuvent être facilement visualisées avec PProf.jl et, comme on le voit ci-dessous, avec l'extension Julia pour VS Code.
Mises à jour du profilage du CPU
Profilage des threads et des tâches
Le profilage du CPU est maintenant collecté avec des métadonnées pour chaque échantillon, y compris l'id du thread et de la tâche, ce qui signifie que les rapports de profilage peuvent être générés pour des threads ou des tâches spécifiques, ou des groupes de ceux-ci.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 | julia> function myfunc() A = rand(200, 200, 400) maximum(A) end myfunc (generic function with 1 method) julia> myfunc() # run once to force compilation 0.9999999942189138 julia> @profile myfunc() 0.9999998811170847 |
L'option d'impression par défaut reste la même, montrant tous les threads et les tâches regroupés (les traces de pile ont été raccourcies pour des raisons de brièveté).
Paquets
Temporisation de la charge du paquet
Un nouvel outil a été ajouté pour donner un aperçu de la façon dont les dépendances de chargement contribuent au temps de chargement d'un paquet. La macro est InteractiveUtils.@time_imports et elle est directement disponible dans le REPL.
julia> @time_imports using CSV
50.7 ms Parsers 17.52% compilation time
0.2 ms DataValueInterfaces
1.6 ms DataAPI
0.1 ms IteratorInterfaceExtensions
0.1 ms TableTraits
17.5 ms Tables
26.8 ms PooledArrays
193.7 ms SentinelArrays 75.12% compilation time
8.6 ms InlineStrings
20.3 ms WeakRefStrings
2.0 ms TranscodingStreams
1.4 ms Zlib_jll
1.8 ms CodecZlib
0.8 ms Compat
13.1 ms FilePathsBase 28.39% compilation time
1681.2 ms CSV 92.40% compilation time
Tout temps de compilation sera mis en évidence en pourcentage du temps de chargement, et si une partie de ce temps de compilation consiste à recompiler des méthodes invalidées, cela sera mis en évidence en pourcentage du temps de compilation.
Mise à jour de l'état des paquets avec indicateur de paquetage évolutif
Les paquets dans Julia ont tendance à déclarer des contraintes de compatibilité sur les versions de leurs dépendances. Ceci est fait pour assurer que vous vous retrouvez avec un ensemble de versions qui devraient bien fonctionner les unes avec les autres. Cependant, cela signifie que parfois le gestionnaire de paquets peut ne pas vous donner la toute dernière version de tous les paquets. Une autre raison pour laquelle vous pouvez ne pas avoir la dernière version d'un paquet est que de nouvelles versions ont été publiées depuis votre dernière mise à jour. Cela peut parfois prêter à confusion lorsque vous rencontrez par exemple un bogue qui a été corrigé dans une version ultérieure ou lorsque la documentation du paquet ne correspond pas à ce que vous exécutez localement.
Par conséquent, le gestionnaire de paquets affichera désormais un petit indicateur lors de l'installation de paquets ou de l'utilisation de la sortie d'état (pkg> st) pour les paquets qui ne sont pas à la dernière version. Il essaiera également de prédire si le paquetage a une chance d'être mis à jour (pkg> up) ou si d'autres paquetages dans votre environnement le « retiennent » (ont des contraintes de compatibilité qui empêchent le paquetage d'être mis à jour).
Il y a aussi un nouveau drapeau --outdated qui peut être passé à l'impression d'état pour voir quelles sont les dernières versions et quelles sont les contraintes de compatibilité qui empêchent les autres paquets de se mettre à jour.
Ci-dessus, nous pouvons voir que Plots et NanMath ne sont pas sur leur dernière version et que Plots et RecipesPipeline empêchent NanMath de se mettre à jour.
Support Pkg pour sysimages
À l'aide du paquet PackageCompiler.jl, il est possible de créer une "sysimage" (un fichier sérialisé précuit) avec les paquets, ce qui peut améliorer considérablement le temps de chargement de ces paquets. L'inconvénient est que lorsqu'on utilise un sysimage personnalisé avec des paquets, la version de ces paquets est "gelée", de sorte que quelle que soit la version du paquet installée dans votre environnement, vous finirez toujours par charger la version qui se trouve dans le sysimage. Explicitement, si vous avez la version 0.1 d'un paquet dans le sysimage et que vous ajoutez la version 0.2 avec le gestionnaire de paquets, vous utiliserez toujours la version 0.1.
Dans la version 1.8, le gestionnaire de paquets comprend quand un sysimage personnalisé est utilisé et n'installera pas de paquets pour vous à des versions différentes de celles qui sont dans le sysimage chargé.
Précompilation améliorée
Julia précompile les paquets en sauvegardant les définitions de vos modules dans un second format (fichiers cache avec l'extension .ji) qui peut être chargé plus rapidement que les fichiers sources bruts. Ces fichiers de cache incluent les modules, les types, les méthodes, les variables globales, et d'autres objets dans le paquet. Pour réduire le temps de la première exécution des méthodes du paquetage, les développeurs ont depuis longtemps l'option d'enregistrer également certains des résultats de l'inférence de type : on peut ajouter des directives explicites de précompilation ou insérer une petite charge de travail qui déclenche la compilation des méthodes.
Malheureusement, les anciennes versions de Julia rejetaient une grande partie de ce code compilé : le code d'inférence de type n'était sauvegardé que pour les méthodes définies dans le paquet, et pour toutes les autres méthodes, il était écarté. Cela signifie que si votre paquetage avait besoin d'une méthode de Base ou d'un autre paquetage qui n'avait pas été précédemment inférée pour les types spécifiques nécessaires à votre paquetage, vous n'aviez pas de chance - cette combinaison méthode/type serait toujours fraîchement inférée dans chaque nouvelle session Julia. De plus, la précompilation a occasionnellement déclenché une baisse de performance à l'exécution : lorsque le compilateur de Julia avait besoin de code inféré par type qui avait été écarté, il a traité l'omission en insérant une invocation indirecte dans les instructions de bas niveau du CPU qu'il compile pour votre méthode. Alors que cela fournissait une opportunité de réinférer les méthodes pour les types requis, l'invocation indirecte allouait de la mémoire et réduisait les performances pour toutes les utilisations ultérieures de la méthode.
Julia 1.8 aborde ces deux limitations en sauvegardant automatiquement tout le code inféré par type qui renvoie au paquetage : soit des méthodes appartenant au paquetage, soit tout code nouvellement inféré dont le compilateur peut prouver qu'il est appelé par des méthodes du paquetage. Plus spécifiquement, tout le code qui est lié à votre paquet par une chaîne d'inférence de type réussie sera mis en cache ; Julia ne rejette que les résultats d'inférence de type pour les méthodes dans d'autres paquets qui ont été seulement appelées par la distribution d'exécution. Cette exigence de chaîne d'inférence assure que la compilation qui est effectuée pour construire votre paquet, mais qui n'est pas nécessaire pour l'exécuter (par exemple, le code utilisé seulement pour la métaprogrammation ou la génération de données), ne gonfle pas inutilement les fichiers .ji.
Avec Julia 1.8, pour les charges de travail avec des types « prévisibles », il est maintenant possible d'éliminer complètement l'inférence de type comme source de latence. Le montant des économies dépend de la part de la latence globale due à l'inférence de type ; dans des tests avec une douzaine de paquets différents, nous avons observé des réductions du temps pour une charge de travail initiale allant de quelques pourcents à 20 fois. Les utilisateurs des outils d'analyse de SnoopCompile constateront également que les résultats de l'ajout de la précompilation sont beaucoup plus prévisibles : pour les dispatch-trees qui remontent aux méthodes appartenant à votre paquet, l'ajout de la précompilation éliminera tout leur temps d'inférence. Ainsi, vous pouvez éliminer les pires contrevenants avec la certitude que votre intervention aura l'effet escompté. Pour ceux qui souhaitent en savoir plus sur la précompilation, cet article de blog et/ou la documentation de SnoopCompile peuvent être utiles.
Amélioration du support pour Apple Silicon
Précédemment, Julia 1.7 offrait la première prévisualisation expérimentale des constructions natives de Julia sur Apple Silicon. Bien que cela ait généralement fonctionné pour une utilisation de base, les utilisateurs ont rencontré de fréquentes erreurs de segmentation, affectant négativement l'expérience. Ces problèmes étaient dus à la façon dont Julia utilise en interne LLVM pour générer et lier le code pour cette plateforme et ont été finalement résolus dans Julia 1.8 en passant à un linker plus moderne, qui a un meilleur support pour les CPU ARM sur macOS. Cependant, cette correction a nécessité une mise à niveau vers LLVM 13, un changement qui ne peut pas être rétroporté vers la série v1.7. Par conséquent, la version 1.7 sera toujours affectée par de fréquents plantages sur Apple Silicon.
Avec la 1.8, Apple Silicon devient une plateforme supportée de niveau 2 et est maintenant couverte par l'intégration continue sur des machines Apple Silicon dédiées.
Source : Julia
Et vous ?
Quel est votre avis sur le sujet ?
Voir aussi :
La version 1.7 du langage Julia est disponible, elle apporte l'installation automatique de paquets, un nouveau format du manifeste et l'ajout des atomiques comme caractéristique du langage
La version 1.6 du langage Julia est disponible, elle apporte une réduction de la latence des compilateurs et supprime les recompilations inutiles
L'équipe de développement de Julia a publié la première version admissible de Julia 1.7, mais elle n'est pas prête pour la production comme c'était le cas avec les précédentes RC