Dans Julia, la précompilation consiste à compiler le code du paquetage et à enregistrer la sortie compilée sur le disque sous la forme d'un "fichier cache" pour chaque paquetage. Ce processus réduit considérablement le temps nécessaire à la compilation lors de l'utilisation du paquet, car vous n'avez besoin de le construire qu'une seule fois et vous pouvez le réutiliser plusieurs fois.
Cependant, avant Julia 1.9, seule une partie du code compilé pouvait être sauvegardée : les types, les variables et les méthodes étaient sauvegardés, ainsi que les résultats de toute inférence de type pour les types d'arguments spécifiquement précompilés par les développeurs du paquetage. Le code natif, c'est-à-dire le code qui s'exécute réellement sur votre processeur, est notablement absent des fichiers de cache. Bien que la mise en cache ait aidé à réduire la latence TTFX (Time-to-first-execution), la nécessité de régénérer le code natif à chaque session signifiait que de nombreux packages souffraient encore de longues latences TTFX.
Avec l'introduction de Julia 1.9, la mise en cache du code natif est désormais disponible, ce qui se traduit par une amélioration significative de la latence TTFX et ouvre la voie à de futures améliorations dans l'ensemble de l'écosystème. Les auteurs de paquets peuvent désormais utiliser des instructions de précompilation ou des charges de travail avec PrecompileTools pour mettre en cache les routines importantes à l'avance. Les utilisateurs peuvent également créer des paquets locaux "Startup" personnalisés qui chargent les dépendances et précompilent les charges de travail adaptées à leur travail quotidien.
Cette fonctionnalité s'accompagne de certains compromis, tels que l'augmentation du temps de précompilation de 10 à 50 %. Toutefois, comme il s'agit d'un coût unique, le jeu en vaut la chandelle. Les fichiers de cache sont également devenus plus volumineux en raison du stockage d'un plus grand nombre de données et de l'utilisation d'un format de sérialisation différent.
Le graphique ci-dessous illustre les changements dans le temps de chargement (TTL), le TTFX et la taille des fichiers de cache à partir de Julia 1.7 (avant toute amélioration récente de la précompilation) :
(Pour la plupart des paquets, TTFX est passé du statut de facteur dominant à celui de facteur pratiquement négligeable. Le TTL a également été réduit, mais pas de manière aussi spectaculaire que le TTFX. Les mêmes données sont présentées dans le tableau ci-dessous, les colonnes "ratio" représentant le rapport Julia 1.7 / Julia 1.9 et "total" signifiant "TTL + TTFX".
Ces chiffres révèlent une énorme amélioration de la qualité de vie dans une large gamme de paquets.
Avec PrecompileTools.jl, Julia 1.9 offre de nombreux avantages de PackageCompiler sans nécessiter de personnalisation de la part de l'utilisateur. Voici une comparaison explicite :
La différence de TTL s'explique par le fait que l'image système peut ignorer toutes les vérifications de validation du code qui sont nécessaires lors du chargement des paquets.
Au moment de la sortie de Julia 1.9, seule une petite partie de l'écosystème des paquets a adopté PrecompileTools. Au fur et à mesure que l'utilisation de ces nouveaux outils se généralise, les utilisateurs peuvent s'attendre à des améliorations continues de TTFX.
Méthodologie
Une charge de travail de démonstration a été conçue pour chaque paquetage. Cette charge de travail a été placée dans un paquet Startup et précompilée ; pour l'analyse comparative, nous chargeons le paquet Startup et exécutons la même charge de travail. Tous les détails peuvent être trouvés dans ce référentiel.
Extensions de paquets
Dans Julia, la puissance de la distribution multiple permet d'étendre les fonctionnalités à un large éventail de types. Par exemple, un package de traçage peut vouloir fournir des fonctionnalités pour tracer une grande variété d'objets Julia, dont beaucoup sont définis dans des packages séparés au sein de l'écosystème Julia. De plus, il est possible d'ajouter des versions optimisées de fonctions génériques pour des types spécifiques, tels que StaticArray, où la taille du tableau est connue au moment de la compilation, ce qui conduit à des améliorations significatives des performances.
Pour étendre une méthode à un type, il faut généralement importer le paquetage contenant le type, charger le paquetage pour accéder au type, puis définir la méthode étendue. Si l'on prend le cas d'utilisation du package "plotting", cela pourrait ressembler à ce qui suit :
Code : | Sélectionner tout |
1 2 3 4 5 | import Contours function plot(contour::Countours.Contour) ... end |
Julia 1.9 introduit les "extensions de paquets", une fonctionnalité qui, au sens large, charge automatiquement un module lorsqu'un ensemble de paquets est chargé. Le module, contenu dans un fichier dans le répertoire ext du paquetage parent, charge la "dépendance faible" et étend les méthodes. L'objectif est de ne pas avoir à payer pour des fonctionnalités que l'on n'utilise pas. Les extensions de paquetages offrent des fonctionnalités similaires à celles que Requires.jl propose déjà, mais avec des avantages clés, tels que la précompilation du code conditionnel et l'ajout de contraintes de compatibilité sur les dépendances faibles. La fonctionnalité d'extension de paquetage étant désormais de "première classe", les auteurs de paquetages devraient être moins réticents à commencer à l'utiliser qu'avec Requires.jl. Les extensions de paquetages peuvent être introduites d'une manière totalement rétrocompatible où l'on peut choisir d'utiliser Requires.jl ou de faire de la dépendance faible une dépendance normale sur les versions antérieures de Julia.
Comme exemple concret où les extensions de paquetage sont utilisées à bon escient, le paquetage ForwardDiff.jl fournit des routines optimisées pour la différenciation automatique lorsque l'entrée est un StaticArray. Dans Julia 1.8, il chargeait inconditionnellement le package StaticArrays, alors que dans 1.9, il utilise une extension de package. Il en résulte une amélioration significative du temps de chargement :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 | # 1.8 (StaticArrays unconditionally loaded) julia> @time using ForwardDiff 0.590685 seconds (2.76 M allocations: 201.567 MiB) # 1.9 (StaticArrays not loaded) julia> @time using ForwardDiff 0.247568 seconds (220.93 k allocations: 13.793 MiB) |
Êtes-vous curieux de savoir comment votre mémoire est utilisée dans vos programmes Julia ? Avec l'introduction de Julia 1.9, vous pouvez désormais générer des Heap snapshot qui peuvent être examinés à l'aide de Chrome DevTools.
Pour créer un instantané du tas, il suffit d'utiliser le package Profile et d'appeler la fonction take_heap_snapshot, comme indiqué ci-dessous :
Code : | Sélectionner tout |
1 2 | using Profile Profile.take_heap_snapshot("Snapshot.heapsnapshot") |
Code : | Sélectionner tout |
Profile.take_heap_snapshot("Snapshot.heapsnapshot", all_one=true)
Indication d'utilisation de la mémoire pour le GC avec --heap-size-hint
Julia 1.9 introduit un nouveau drapeau de commande, --heap-size-hint=<size>, qui permet aux utilisateurs de fixer une limite d'utilisation de la mémoire, au-delà de laquelle le garbage collector (GC) travaillera plus agressivement pour nettoyer la mémoire inutilisée.
En spécifiant une limite de mémoire, les utilisateurs peuvent s'assurer que le garbage collector gère les ressources mémoire de manière plus proactive, réduisant ainsi le risque de manquer de mémoire.
Pour utiliser cette nouvelle fonctionnalité, il suffit de lancer Julia avec l'option --heap-size-hint suivie de la limite de mémoire souhaitée :
Code : | Sélectionner tout |
julia --heap-size-hint=<size>
Cette amélioration dans Julia 1.9 rend plus facile que jamais la gestion efficace des ressources mémoire, fournissant aux utilisateurs un meilleur contrôle et une plus grande flexibilité lorsqu'ils travaillent avec des applications gourmandes en mémoire.
Cette fonctionnalité a été introduite dans #45369.
Trier les performances
L'algorithme de tri par défaut a été remplacé par un algorithme de tri plus adaptatif qui est toujours stable et dont les performances sont souvent à la pointe de la technologie. Pour les types et ordres simples - BitInteger, IEEEFloat et Char triés dans l'ordre par défaut ou dans l'ordre inverse - nous utilisons un tri radix dont le temps d'exécution est linéaire par rapport à la taille de l'entrée. Cet effet est particulièrement prononcé pour Float16s qui a bénéficié d'une accélération de 3x-50x par rapport à la version 1.8.
Pour les autres types, l'algorithme de tri par défaut a été remplacé par l'algorithme interne ScratchQuickSort dans la plupart des cas, qui est stable et généralement plus rapide que QuickSort, bien qu'il alloue de la mémoire. Dans les situations où l'efficacité de la mémoire est cruciale, vous pouvez ignorer ces nouvelles valeurs par défaut en spécifiant alg=QuickSort.
Pour en savoir plus sur ces changements, vous pouvez regarder la conférence JuliaCon 2022, Julia's latest in high performance sorting et sa suite à venir dans JuliaCon 2023.
Les tâches et le pool de discussion interactif
Avant la version 1.9, Julia traitait toutes les tâches de la même manière, les exécutant sur tous les fils d'exécution disponibles sans distinction de priorité. Cependant, dans certaines situations, vous pouvez souhaiter que certaines tâches soient prioritaires, par exemple lors de l'exécution d'un heartbeat, de la fourniture d'une interface interactive ou de l'affichage d'une mise à jour de la progression.
Pour répondre à ce besoin, vous pouvez désormais désigner une tâche comme interactive lorsque vous la Threads.@spawn :
Code : | Sélectionner tout |
1 2 | using Base.Threads @spawn :interactive f() |
Code : | Sélectionner tout |
julia --threads 3,1
Pour plus d'informations, reportez-vous à la section du manuel sur le multithreading. Cette fonctionnalité a été introduite dans #42302 .
REPL
Module contextuel REPL
Dans Julia, la REPL évalue par défaut les expressions à l'intérieur du module "Main". À partir de la version 1.9, vous pouvez maintenant changer cela pour n'importe quel autre module. De nombreuses méthodes d'introspection, telles que varinfo, qui auparavant examinaient par défaut le module Main, seront désormais évaluées par défaut dans le module contextuel actuel de la REPL.
Cette fonctionnalité peut être particulièrement utile lors du développement d'un paquetage, car vous pouvez définir le paquetage comme module contextuel courant. Pour changer de module, il suffit d'entrer le nom du module dans la REPL et d'exécuter Meta+M (souvent Alt+M), ou d'utiliser la commande REPL.activate.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | julia> @__MODULE__ # Shows module where macro is expanded Main # Typing Base.Math and pressing Meta-m changes the context module (Base.Math) julia> @__MODULE__ Base.Math (Base.Math) julia> varinfo() name size summary ––––––––––– ––––––– ––––––––––––––––––––––––––––––––––––––––––––– @evalpoly 0 bytes @evalpoly (macro with 1 method) Math Module ^ 0 bytes ^ (generic function with 68 methods) acos 0 bytes acos (generic function with 12 methods) acosd 0 bytes acosd (generic function with 1 method) acosh 0 bytes acosh (generic function with 12 methods) acot 0 bytes acot (generic function with 4 methods) ... |
S'inspirant fortement du shell IPython (et d'autres systèmes basés sur des carnets de notes comme Mathematica), le REPL de Julia peut activer une "invite numérotée" qui stocke les objets évalués dans le REPL pour une utilisation ultérieure et garde une trace du nombre d'expressions qui ont été évaluées.
Pouvoir se référer à un objet évalué plus tôt peut être utile si, par exemple, on oublie de stocker le résultat d'un long calcul dans une variable et que l'on exécute ensuite autre chose (de sorte que le résultat est écrasé).
DelimitedFiles - première stdlib à être mise à jour
Julia est livrée avec un ensemble de bibliothèques standard ("stdlibs" qui sont similaires à des paquets normaux sauf qu'elles peuvent être chargées sans avoir à les installer explicitement. La plupart de ces stdlibs sont également "prebaked" dans le sysimage fourni par Julia, ce qui signifie qu'elles sont techniquement chargées à chaque fois que Julia est démarrée.
Cependant, cette approche a quelques inconvénients :
- les versions des stdlib sont liées à la version de Julia, ce qui oblige les utilisateurs à attendre la prochaine version de Julia pour recevoir les corrections de bogues ;
- l'intégration des stdlibs dans la sysimage entraîne un coût pour les utilisateurs qui ne les utilisent pas, car elles sont chargées à chaque démarrage de Julia ;
- le développement de stdlibs qui sont dans la sysimage peut être ennuyeux.
Dans la version 1.9, nous expérimentons un nouveau concept de "stdlibs upgradables" qui sont livrés avec Julia mais qui peuvent aussi être mis à jour comme des paquets normaux. Pour commencer, ceci est fait avec la petite stdlib DelimitedFiles, relativement peu utilisée.
En commençant par une nouvelle installation de Julia, nous pouvons voir que le paquet DelimitedFiles est chargeable et qu'il est chargé à partir de l'installation de Julia :
Code : | Sélectionner tout |
1 2 3 4 5 | julia> using DelimitedFiles [ Info: Precompiling DelimitedFiles [8bb1440f-4735-579b-a4ab-409b98df4dab] julia> pkgdir(DelimitedFiles) "/Users/kc/julia/share/julia/stdlib/v1.9/DelimitedFiles" |
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 | (@v1.9) pkg> add DelimitedFiles Resolving package versions... Updating `~/.julia/environments/v1.9/Project.toml` [8bb1440f] + DelimitedFiles v1.9.1 Updating `~/.julia/environments/v1.9/Manifest.toml` [8bb1440f] + DelimitedFiles v1.9.1 Precompiling environment... 1 dependency successfully precompiled in 1 seconds. 59 already precompiled. julia> using DelimitedFiles julia> pkgdir(DelimitedFiles) "/Users/kristoffercarlsson/.julia/packages/DelimitedFiles/aGcsu" |
--math-mode=fast est maintenant un no-op (#41638). L'équipe est arrivée à la conclusion qu'une option fastmath globale est impossible à utiliser correctement dans Julia. Par exemple, cela peut conduire à des surprises telles que exp retournant complètement la mauvaise valeur comme rapporté dans #41592. La combinaison d'une option fastmath à l'exécution avec la précompilation et la propagation des constantes conduit à des incohérences à moins que nous acceptions le coût d'une image système entièrement séparée.
Les utilisateurs sont encouragés à utiliser la macro @fastmath à la place, qui limite les effets de fastmath à un petit morceau de code.
Pkg
pkg> up Foo essaiera maintenant de mettre à jour uniquement Foo
Auparavant, il n'était pas précisé quels packages étaient réellement autorisés à être mis à jour lorsqu'on donnait un package spécifique à mettre à jour (pkg> up Foo). Maintenant up Foo n'autorise que Foo lui-même à se mettre à jour avec tous les autres packages ayant leur version corrigée dans le processus de résolution. Il est possible d'assouplir cette restriction avec les différentes options de la commande --preserve pour permettre par exemple aux dépendances de Foo de se mettre à jour. Voir la documentation de Pkg.update pour plus d'informations.
pkg> add ne mettra à jour automatiquement le registre qu'une fois par jour.
Pkg se souviendra désormais de la dernière fois que le registre a été mis à jour à travers les sessions de julia et n'effectuera une mise à jour automatique qu'une fois par jour lors de l'utilisation d'une commande add. Auparavant, le registre se mettait à jour automatiquement une fois par session. Notez que la commande update essaiera toujours de mettre à jour le registre.
pkg> add peut maintenant essayer d'ajouter uniquement des packages déjà installés
Lorsque l'on travaille avec de nombreux environnements, par exemple à travers les ordinateurs portables Pluto, le comportement par défaut de Pkg.add d'ajouter la dernière version du paquetage demandé et toutes les nouvelles dépendances peut signifier l'utilisation fréquente de la précompilation.
Il est maintenant possible de dire à Pkg.add de préférer ajouter des versions déjà installées de paquetages (ceux qui ont déjà été téléchargés sur votre machine), qui sont plus susceptibles d'être précompilés.
Pour accepter globalement cette nouvelle préférence, mettez la variable env JULIA_PKG_PRESERVE_TIERED_INSTALLED à true.
Ou pour permettre l'utilisation d'opérations spécifiques :
- pkg> add --preserve=tiered_installed Foo pour essayer cette nouvelle stratégie en premier dans la préservation à plusieurs niveaux;
- pkg> add --preserve=installed Foo pour essayer strictement cette stratégie, ou erreur.
Notez qu'en utilisant cette méthode, vous risquez d'installer de vieilles versions de packages, donc si vous rencontrez des problèmes, c'est généralement une bonne idée de faire une mise à jour vers la dernière version pour voir si le problème a été corrigé.
pkg> why pour vous dire pourquoi un paquetage est dans le manifeste
Pour montrer la raison pour laquelle un package est dans le manifeste, une nouvelle commande pkg> why Foo est disponible. La sortie est toutes les différentes façons d'atteindre le paquetage à travers le graphe de dépendance en commençant par les dépendances directes.
Code : | Sélectionner tout |
1 2 3 4 | (jl_zMxmBY) pkg> why DataAPI CSV → PooledArrays → DataAPI CSV → Tables → DataAPI CSV → WeakRefStrings → DataAPI |
Auparavant, les tests de couverture ne pouvaient être activés que pour all où tout le code visité est vérifié, user (l'ancienne valeur par défaut de Pkg.test) où tout sauf Base est vérifié y compris stdlibs, ou none où le suivi est désactivé.
L'action github julia-runtest active par défaut les tests de couverture, ce qui signifie que beaucoup de suivi en dehors du paquetage testé avait lieu auparavant, ce qui ralentissait les tests, en particulier les boucles serrées.
La version 1.8 a introduit la possibilité de spécifier un chemin vers un fichier ou un répertoire pour le suivi de la couverture via --code-coverage=@path, et la version 1.9 en fait la valeur par défaut de Pkg.test(coverage=true) (et donc utilisée par julia-runtest par défaut).
Ce changement signifie que beaucoup moins de code doit être suivi, et dans les cas où le code des dépendances tombe dans des boucles serrées, cela peut accélérer considérablement la suite de tests. Dans un exemple, les tests d'Octavian.jl avec la couverture activée sont passés de >2 heures à ~6 minutes.
Apple Silicon atteint le statut de niveau 1
Avec tous les tests réussis et l'intégration continue (CI) établie pour Apple Silicon, le statut de la plate-forme est passé du niveau 2 au niveau 1.
Mise à jour LLVM vers v14
LLVM est l'infrastructure sous-jacente du compilateur sur laquelle le compilateur de Julia s'appuie. Avec Julia 1.9, nous mettons à jour la version utilisée vers la v14.0.6.
Parmi les autres fonctionnalités introduites dans LLVM 14, il y a l'autovectorisation activée par défaut pour les extensions SVE/SVE2 sur les processeurs AArch64. SVE, Scalable Vector Extension, est une extension de type SIMD qui utilise des registres vectoriels à largeur flexible, au lieu des registres à largeur fixe généralement utilisés par d'autres architectures SIMD. Le code Julia n'a rien à faire pour utiliser les instructions SVE/SVE2 : le code vectorisable utilise toujours les instructions SIMD lorsque cela est possible, et avec LLVM 14, les registres SVE seront utilisés de manière plus agressive sur les processeurs qui le prennent en charge, tels que l'A64FX de Fujitsu, Nvidia Grace, ou la série ARM Neoverse. Nous avons donné un aperçu des capacités d'autovectorisation de Julia SVE dans le webinaire Julia sur A64FX.
Arithmétique à virgule flottante demi-précision native
Pour exécuter des opérations arithmétiques sur des valeurs Float16, Julia les promouvait en Float32, puis les reconvertissait en Float16 pour renvoyer le résultat. 1.9 a ajouté la prise en charge des opérations natives Float16 sur les processeurs AArch64 qui prennent en charge matériellement l'arithmétique à virgule flottante demi-précision, comme la série M d'Apple ou l'A64FX de Fujitsu. Dans les applications liées à la mémoire, cela permet une accélération jusqu'à 2 × par rapport aux opérations Float32 et 4 × par rapport aux opérations Float64. Par exemple, sur un MacBook M1, vous pouvez obtenir
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | julia> using BenchmarkTools julia> function sumsimd(v) sum = zero(eltype(v)) @simd for x in v sum += x end return sum end sumsimd (generic function with 1 method) julia> @btime sumsimd(x) setup=(x=randn(Float16, 1_000_000)) 58.416 μs (0 allocations: 0 bytes) Float16(551.0) julia> @btime sumsimd(x) setup=(x=randn(Float32, 1_000_000)) 116.916 μs (0 allocations: 0 bytes) 897.7202f0 julia> @btime sumsimd(x) setup=(x=randn(Float64, 1_000_000)) 234.125 μs (0 allocations: 0 bytes) 1164.2247860232349 |
Source : Julia
Et vous ?
Que pensez-vous du langage de programmation Julia ?
Voyez-vous en ce langage un potentiel pour l'avenir de la programmation ?
Quelle amélioration de la version 1.9 vous intéresse-t-elle le plus ?
Voir aussi :
La version 1.8 du langage Julia est disponible, elle apporte la temporisation de la charge du paquet, une amélioration du support pour Apple Silicon, un nouveau planificateur par défaut pour @threads
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
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