Partage via


Nouveautés du runtime .NET 9

Cet article décrit les nouvelles fonctionnalités et améliorations des performances dans le runtime .NET pour .NET 9.

Modèle d’attribut pour les commutateurs de fonctionnalités avec prise en charge du découpage

Deux nouveaux attributs permettent de définir des commutateurs de fonctionnalités que les bibliothèques .NET (et vous) pouvez utiliser pour basculer des zones de fonctionnalité. Si une fonctionnalité n’est pas prise en charge, les fonctionnalités non prises en charge (et donc inutilisées) sont supprimées lors de la suppression ou de la compilation avec AOT natif, ce qui réduit la taille de l’application.

  • FeatureSwitchDefinitionAttribute est utilisé pour traiter une propriété de commutateur de fonctionnalité comme une constante lors de la suppression, et le code mort qui est protégé par le commutateur peut être supprimé :

    if (Feature.IsSupported)
        Feature.Implementation();
    
    public class Feature
    {
        [FeatureSwitchDefinition("Feature.IsSupported")]
        internal static bool IsSupported => AppContext.TryGetSwitch("Feature.IsSupported", out bool isEnabled) ? isEnabled : true;
    
        internal static void Implementation() => ...;
    }
    

    Lorsque l’application est rognée avec les paramètres de fonctionnalité suivants dans le fichier projet, Feature.IsSupported est traitée comme false, et Feature.Implementation le code est supprimé.

    <ItemGroup>
      <RuntimeHostConfigurationOption Include="Feature.IsSupported" Value="false" Trim="true" />
    </ItemGroup>
    
  • FeatureGuardAttribute est utilisé pour traiter une propriété de commutateur de caractéristiques comme un garde pour le code annoté avec RequiresUnreferencedCodeAttribute, RequiresAssemblyFilesAttributeou RequiresDynamicCodeAttribute. Par exemple:

    if (Feature.IsSupported)
        Feature.Implementation();
    
    public class Feature
    {
        [FeatureGuard(typeof(RequiresDynamicCodeAttribute))]
        internal static bool IsSupported => RuntimeFeature.IsDynamicCodeSupported;
    
        [RequiresDynamicCode("Feature requires dynamic code support.")]
        internal static void Implementation() => ...; // Uses dynamic code
    }
    

    Lorsqu’il est généré avec <PublishAot>true</PublishAot>, l’appel à Feature.Implementation() ne produit pas d’avertissement d’analyseur IL3050 et Feature.Implementation le code est supprimé lors de la publication.

UnsafeAccessorAttribute prend en charge les paramètres génériques

La UnsafeAccessorAttribute fonctionnalité permet un accès non sécurisé aux membres de type inaccessibles à l’appelant. Cette fonctionnalité a été conçue dans .NET 8, mais implémentée sans prise en charge des paramètres génériques. .NET 9 ajoute la prise en charge des paramètres génériques pour les scénarios AOT CoreCLR et natifs. Le code suivant montre un exemple d’utilisation.

using System.Runtime.CompilerServices;

public class Class<T>
{
    private T? _field;
    private void M<U>(T t, U u) { }
}

class Accessors<V>
{
    [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_field")]
    public extern static ref V GetSetPrivateField(Class<V> c);

    [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "M")]
    public extern static void CallM<W>(Class<V> c, V v, W w);
}

internal class UnsafeAccessorExample
{
    public void AccessGenericType(Class<int> c)
    {
        ref int f = ref Accessors<int>.GetSetPrivateField(c);

        Accessors<int>.CallM<string>(c, 1, string.Empty);
    }
}

Collecte des déchets

L’adaptation dynamique aux tailles d’application (DATAS) est désormais activée par défaut. Il vise à s’adapter aux besoins en mémoire de l’application, ce qui signifie que la taille du tas d’application doit être proportionnelle à la taille des données de longue durée. DATAS a été introduit en tant que fonctionnalité d’opt-in dans .NET 8 et a été considérablement mis à jour et amélioré dans .NET 9.

Pour plus d’informations, consultez Adaptation dynamique aux tailles d’application (DATAS).

Technologie d’application des flux de contrôle

La technologie d’application du flux de contrôle (CET) est activée par défaut pour les applications sur Windows. Il améliore considérablement la sécurité en ajoutant une protection de pile appliquée par le matériel contre les attaques de programmation orientée retour (ROP). Il s’agit de la dernière atténuation de sécurité du runtime .NET.

CET impose certaines limitations sur les processus activés par CET et peut entraîner une petite régression des performances. Il existe différents contrôles pour refuser cet accès.

Comportement de recherche d’installation .NET

Les applications .NET peuvent désormais être configurées pour la façon dont elles doivent rechercher le runtime .NET. Cette fonctionnalité peut être utilisée avec des installations d’exécution privées ou pour contrôler plus fortement l’environnement d’exécution.

Améliorations des performances

Les améliorations de performances suivantes ont été apportées pour .NET 9 :

Optimisations des boucles

L’amélioration de la génération de code pour les boucles est une priorité pour .NET 9. Les améliorations suivantes sont désormais disponibles :

Note

L’élargissement des variables d’induction et l’adressage post-indexé sont similaires : ils optimisent tous les deux les accès à la mémoire avec des variables d’index de boucle. Toutefois, ils prennent différentes approches, car Arm64 offre une fonctionnalité processeur et x64 ne le fait pas. L’élargissement des variables d’induction a été implémenté pour x64 en raison des différences dans la capacité processeur/ISA et les besoins.

Élargissement de la variable d’induction

Le compilateur 64 bits propose une nouvelle optimisation appelée élargissement de la variable d’induction (IV).

Un IV est une variable dont la valeur change en tant qu’itération de boucle conteneur. Dans la boucle suivante for , i est un IV : for (int i = 0; i < 10; i++). Si le compilateur peut analyser l’évolution de la valeur d’un IV sur les itérations de sa boucle, il peut produire du code plus performant pour les expressions associées.

Prenons l’exemple suivant qui itère dans un tableau :

static int Sum(int[] nums)
{
    int sum = 0;
    for (int i = 0; i < nums.Length; i++)
    {
        sum += nums[i];
    }

    return sum;
}

La variable d’index, est ide 4 octets de taille. Au niveau de l’assembly, les registres 64 bits sont généralement utilisés pour contenir des index de tableau sur x64 et, dans les versions précédentes de .NET, le compilateur a généré du code qui s’étendait i à 8 octets pour l’accès au tableau, mais continuait à traiter i comme un entier de 4 octets ailleurs. Toutefois, l’extension i à 8 octets nécessite une instruction supplémentaire sur x64. Avec l’élargissement iv, le compilateur JIT 64 bits s’étend i désormais à 8 octets tout au long de la boucle, omettant l’extension zéro. Le bouclage sur les tableaux est très courant et les avantages de cette suppression d’instructions s’ajoutent rapidement.

Adressage post-indexé sur Arm64

Les variables d’index sont fréquemment utilisées pour lire des régions séquentielles de mémoire. Considérez la boucle idiomatique for :

static int Sum(int[] nums)
{
    int sum = 0;
    for (int i = 0; i < nums.Length; i++)
    {
        sum += nums[i];
    }

    return sum;
}

Pour chaque itération de la boucle, la variable i d’index est utilisée pour lire un entier, numspuis i est incrémentée. Dans l’assembly Arm64, ces deux opérations se présentent comme suit :

ldr w0, [x1]
add x1, x1, #4

ldr w0, [x1] charge l’entier à l’adresse mémoire dans x1w0; cela correspond à l’accès du nums[i] code source. Ensuite, add x1, x1, #4 augmente l’adresse en x1 quatre octets (la taille d’un entier), en passant à l’entier suivant en nums. Cette instruction correspond à l’opération exécutée à la i++ fin de chaque itération.

Arm64 prend en charge l’adressage post-indexé, où le registre « index » est automatiquement incrémenté une fois son adresse utilisée. Cela signifie que deux instructions peuvent être combinées en une seule, ce qui rend la boucle plus efficace. Le processeur ne doit décoder qu’une seule instruction au lieu de deux, et le code de la boucle est désormais plus convivial.

Voici à quoi ressemble l’assembly mis à jour :

ldr w0, [x1], #0x04

À #0x04 la fin, l’adresse est x1 incrémentée de quatre octets après son utilisation pour charger un entier dans w0. Le compilateur 64 bits utilise désormais l’adressage post-indexé lors de la génération du code Arm64.

Réduction de la force

La réduction de la force est une optimisation du compilateur où une opération est remplacée par une opération plus rapide et logiquement équivalente. Cette technique est particulièrement utile pour optimiser les boucles. Considérez la boucle idiomatique for :

static int Sum(int[] nums)
{
    int sum = 0;
    for (int i = 0; i < nums.Length; i++)
    {
        sum += nums[i];
    }

    return sum;
}

Le code d’assembly x64 suivant montre un extrait de code généré pour le corps de la boucle :

add ecx, dword ptr [rax+4*rdx+0x10]
inc edx

Ces instructions correspondent aux expressions sum += nums[i] et i++, respectivement. rcx (ecx contient les 32 bits inférieurs de ce registre) contient la valeur de sum, rax contient l’adresse de base de nums, et rdx contient la valeur de i. Pour calculer l’adresse de nums[i], l’index est rdxmultiplié par quatre (taille d’un entier). Ce décalage est ensuite ajouté à l’adresse de base dans rax, ainsi qu’un remplissage. (Une fois l’entier lu nums[i] , il est ajouté et rcx l’index est rdx incrémenté.) En d’autres termes, chaque accès au tableau nécessite une multiplication et une opération d’ajout.

La multiplication est plus coûteuse que l’addition et le remplacement de l’ancien par celui-ci est une motivation classique pour la réduction de la force. Pour éviter le calcul de l’adresse de l’élément sur chaque accès mémoire, vous pouvez réécrire l’exemple pour accéder aux entiers à nums l’aide d’un pointeur plutôt qu’une variable d’index :

static int Sum2(Span<int> nums)
{
    int sum = 0;
    ref int p = ref MemoryMarshal.GetReference(nums);
    ref int end = ref Unsafe.Add(ref p, nums.Length);
    while (Unsafe.IsAddressLessThan(ref p, ref end))
    {
        sum += p;
        p = ref Unsafe.Add(ref p, 1);
    }

    return sum;
}

Le code source est plus compliqué, mais il est logiquement équivalent à l’implémentation initiale. En outre, l’assembly semble mieux :

add ecx, dword ptr [rdx]
add rdx, 4

rcx (ecx contient les 32 bits inférieurs de ce registre) conserve toujours la valeur de sum, mais rdx maintenant contient l’adresse pointée par p, de sorte que l’accès aux éléments dans nums simplement nous oblige à déréférencer rdx. Toutes les multiplications et ajouts du premier exemple ont été remplacés par une seule add instruction pour déplacer le pointeur vers l’avant.

Dans .NET 9, le compilateur JIT transforme automatiquement le premier modèle d’indexation en seconde sans avoir à réécrire du code.

Direction de la variable du compteur de boucles

Le compilateur 64 bits reconnaît désormais quand la variable de compteur d’une boucle est utilisée uniquement pour contrôler le nombre d’itérations, et transforme la boucle en compte au lieu de monter.

Dans le modèle idiomatique for (int i = ...) , la variable de compteur augmente généralement. Prenons l'exemple suivant :

for (int i = 0; i < 100; i++)
{
    DoSomething();
}

Toutefois, sur de nombreuses architectures, il est plus performant de décrémenter le compteur de la boucle, comme suit :

for (int i = 100; i > 0; i--)
{
    DoSomething();
}

Pour le premier exemple, le compilateur doit émettre une instruction pour incrémenter i, suivie d’une instruction pour effectuer la i < 100 comparaison, suivie d’un saut conditionnel pour continuer la boucle si la condition est toujours true— c’est trois instructions au total. Toutefois, si la direction du compteur est retournée, une instruction inférieure est nécessaire. Par exemple, sur x64, le compilateur peut utiliser l’instruction pour décrémenter dec; lorsqu’elle i atteint zéro, l’instruction i définit un indicateur d’UC qui peut être utilisé comme condition pour une instruction de saut immédiatement après le dec.dec

La réduction de la taille du code est petite, mais si la boucle s’exécute pour un nombre d’itérations nontrivial, l’amélioration des performances peut être significative.

Améliorations de l’inlining

Un des . Les objectifs de NET pour l’inliner du compilateur JIT consiste à supprimer autant de restrictions qui empêchent l’inline d’une méthode que possible. .NET 9 active l’incorporation des points suivants :

  • Génériques partagés qui nécessitent des consultations à l’exécution.

    Prenons l’exemple des méthodes suivantes :

    static bool Test<T>() => Callee<T>();
    static bool Callee<T>() => typeof(T) == typeof(int);
    

    Lorsqu’il T s’agit d’un type de référence tel stringque , le runtime crée des génériques partagés, qui sont des instanciations spéciales de Test et Callee qui sont partagées par tous les types de type T ref. Pour ce faire, le runtime génère des dictionnaires qui mappent des types génériques aux types internes. Ces dictionnaires sont spécialisés par type générique (ou par méthode générique) et sont accessibles au moment de l’exécution pour obtenir des informations sur T et sur les types qui dépendent de T. Historiquement, le code compilé juste-à-temps n'était capable d'effectuer ces consultations dynamiques que sur le dictionnaire de la méthode racine. Cela signifiait que le compilateur JIT n’était pas inline Callee dans Test: il n’y avait aucun moyen pour le code inline d’accéder Callee au dictionnaire approprié, même si les deux méthodes étaient instanciées sur le même type.

    .NET 9 a levé cette restriction en activant librement les recherches de type runtime dans les appelés, ce qui signifie que le compilateur JIT peut désormais inliner des méthodes comme Callee dans Test.

    Supposons que nous appelons Test<string> dans une autre méthode. En pseudocode, l’inlining ressemble à ceci :

    static bool Test<string>() => typeof(string) == typeof(int);
    

    Cette vérification de type peut être calculée pendant la compilation. Le code final ressemble donc à ceci :

    static bool Test<string>() => false;
    

    Les améliorations apportées à l’inliner du compilateur JIT peuvent avoir des effets composés sur d’autres décisions d’incorporation, ce qui entraîne des gains de performances significatifs. Par exemple, la décision d’inline Callee peut également permettre à Test<string> l’appel d’être inline, et ainsi de suite. Cela a produit des centaines d’améliorations de référence, avec au moins 80 benchmarks s’améliorant de 10% ou plus.

  • Accède à des statiques locales de thread sur Windows x64, Linux x64 et Linux Arm64.

    Pour static les membres de classe, il existe exactement une instance du membre sur toutes les instances de la classe, qui « partagent » le membre. Si la valeur d’un static membre est unique à chaque thread, ce qui permet d’améliorer les performances du thread local, car elle élimine la nécessité d’une primitive d’accès concurrentiel pour accéder en toute sécurité au static membre à partir de son thread conteneur.

    Auparavant, les accès aux statiques locales de thread dans les programmes compilés par AOT natifs nécessitaient au compilateur d’émettre un appel dans le runtime pour obtenir l’adresse de base du stockage local de threads. À présent, le compilateur peut inliner ces appels, ce qui entraîne beaucoup moins d’instructions pour accéder à ces données.

Améliorations de PGO : Vérifications de type et casts

L’optimisation dynamique guidée par profil (PGO) est activée pour .NET 8 par défaut. NET 9 développe l’implémentation PGO du compilateur JIT pour profiler davantage de modèles de code. Lorsque la compilation hiérarchisé est activée, le compilateur JIT insère déjà l’instrumentation dans votre programme pour profiler son comportement. Lorsqu’il recompile avec des optimisations, le compilateur tire parti du profil qu’il a créé lors de l’exécution pour prendre des décisions spécifiques à l’exécution actuelle de votre programme. Dans .NET 9, le compilateur JIT utilise des données PGO pour améliorer les performances des vérifications de type.

La détermination du type d’un objet nécessite un appel au runtime, qui est fourni avec une pénalité de performances. Lorsque le type d’un objet doit être vérifié, le compilateur JIT émet cet appel pour la justesse (les compilateurs ne peuvent généralement pas exclure les possibilités, même s’ils semblent improbables). Toutefois, si les données PGO suggèrent qu’un objet est susceptible d’être un type spécifique, le compilateur JIT émet désormais un chemin rapide qui recherche ce type à bon marché et revient sur le chemin lent de l’appel dans le runtime uniquement si nécessaire.

Vectorisation Arm64 dans les bibliothèques .NET

Une nouvelle EncodeToUtf8 implémentation tire parti de la capacité du compilateur JIT à émettre des instructions de chargement/de magasin multiinscription sur Arm64. Ce comportement permet aux programmes de traiter des blocs de données plus volumineux avec moins d’instructions. Les applications .NET sur différents domaines doivent voir des améliorations de débit sur le matériel Arm64 qui prend en charge ces fonctionnalités. Certains benchmarks réduisent leur temps d’exécution de plus de la moitié.

Génération de code Arm64

Le compilateur JIT a déjà la possibilité de transformer sa représentation des charges contiguës afin d’utiliser l’instruction ldp (pour le chargement des valeurs) sur Arm64. .NET 9 étend cette possibilité de stocker des opérations.

L’instruction str stocke les données d’un registre unique en mémoire, tandis que l’instruction stp stocke les données à partir d’une paire de registres. L’utilisation stp au lieu de signifie que la même tâche peut être effectuée avec moins d’opérations de str magasin, ce qui améliore le temps d’exécution. Le rasage d’une instruction peut sembler une petite amélioration, mais si le code s’exécute dans une boucle pour un nombre d’itérations nontrivial, les gains de performances peuvent s’ajouter rapidement.

Par exemple, considérez l’extrait de code suivant :

class Body { public double x, y, z, vx, vy, vz, mass; }

static void Advance(double dt, Body[] bodies)
{
    foreach (Body b in bodies)
    {
        b.x += dt * b.vx;
        b.y += dt * b.vy;
        b.z += dt * b.vz;
    }
}

Les valeurs de , b.xet b.y sont mises à jour dans le corps de b.zla boucle. Au niveau de l’assembly, chaque membre peut être stocké avec une str instruction ; ou en utilisant stp, deux des magasins (b.xet b.y, ou , b.yet b.z , car ces paires sont contiguës en mémoire) peuvent être gérées avec une instruction. Pour utiliser l’instruction stp à stocker dans b.x et b.y simultanément, le compilateur doit également déterminer que les calculs et b.x + (dt * b.vx) sont indépendants les uns b.y + (dt * b.vy) des autres et peuvent être effectués avant de les stocker dans b.x et b.y.

Exceptions plus rapides

Le runtime CoreCLR a adopté une nouvelle approche de gestion des exceptions qui améliore les performances de la gestion des exceptions. La nouvelle implémentation est basée sur le modèle de gestion des exceptions du runtime NativeAOT. La modification supprime la prise en charge de la gestion des exceptions structurées Windows (SEH) et de son émulation sur Unix. La nouvelle approche est prise en charge dans tous les environnements, à l’exception de Windows x86 (32 bits).

La nouvelle implémentation de gestion des exceptions est 2 à 4 fois plus rapide, selon certains micro-benchmarks de gestion des exceptions. Les améliorations de perf suivantes ont été mesurées dans le laboratoire perf :

La nouvelle implémentation est activée par défaut. Toutefois, si vous devez revenir au comportement de gestion des exceptions hérité, vous pouvez le faire de l’une des manières suivantes :

  • Défini System.Runtime.LegacyExceptionHandling sur true le runtimeconfig.json fichier.
  • Définissez la variable d’environnement DOTNET_LegacyExceptionHandling sur 1.

Disposition du code

Les compilateurs raisonnés généralement du flux de contrôle d’un programme à l’aide de blocs de base, où chaque bloc est un bloc de code qui ne peut être entré qu’à la première instruction et quitté par le biais de la dernière instruction. L’ordre des blocs de base est important. Si un bloc se termine par une instruction de branche, le flux de contrôle est transféré vers un autre bloc. L’un des objectifs de la réorganisation des blocs consiste à réduire le nombre d’instructions de branche dans le code généré en optimisant le comportement de basculement . Si chaque bloc de base est suivi de son successeur le plus probable, il peut « tomber dans » son successeur sans avoir besoin d’un saut.

Jusqu’à récemment, la réorganisation du bloc dans le compilateur JIT était limitée par l’implémentation de flowgraph. Dans .NET 9, l’algorithme de réorganisation de bloc du compilateur JIT a été remplacé par une approche plus simple et plus globale. Les structures de données flowgraph ont été refactorisé pour :

  • Supprimez certaines restrictions relatives à l’ordre des blocs.
  • Ingrainer les probabilités d’exécution dans chaque modification de flux de contrôle entre les blocs.

En outre, les données de profil sont propagées et conservées à mesure que le flux de la méthode est transformé.

Réduction de l’exposition aux adresses

Dans .NET 9, le compilateur JIT peut mieux suivre l’utilisation des adresses de variables locales et éviter une exposition inutile aux adresses.

Lorsque l’adresse d’une variable locale est utilisée, le compilateur JIT doit prendre des précautions supplémentaires lors de l’optimisation de la méthode. Par exemple, supposons que le compilateur optimise une méthode qui transmet l’adresse d’une variable locale dans un appel à une autre méthode. Étant donné que l’appelé peut utiliser l’adresse pour accéder à la variable locale, pour maintenir la correction, le compilateur évite de transformer la variable. Les variables locales exposées peuvent considérablement empêcher le potentiel d’optimisation du compilateur.

Prise en charge d’AVX10v1

De nouvelles API ont été ajoutées pour AVX10, qui est un nouveau jeu d’instructions SIMD d’Intel. Vous pouvez accélérer vos applications .NET sur du matériel compatible AVX10 avec des opérations vectorisées à l’aide des nouvelles Avx10v1 API.

Génération de code intrinsèque matériel

De nombreuses API intrinsèques matérielles s’attendent à ce que les utilisateurs passent des valeurs constantes pour certains paramètres. Ces constantes sont encodées directement dans l’instruction sous-jacente de l’intrinsèque, au lieu d’être chargées dans des registres ou accessibles à partir de la mémoire. Si une constante n’est pas fournie, l’intrinsèque est remplacée par un appel à une implémentation de secours qui est fonctionnellement équivalente, mais plus lente.

Prenons l'exemple suivant :

static byte Test1()
{
    Vector128<byte> v = Vector128<byte>.Zero;
    const byte size = 1;
    v = Sse2.ShiftRightLogical128BitLane(v, size);
    return Sse41.Extract(v, 0);
}

L’utilisation de size l’appel peut Sse2.ShiftRightLogical128BitLane être remplacée par la constante 1 et, dans des circonstances normales, le compilateur JIT est déjà capable de cette optimisation de substitution. Toutefois, lorsque vous déterminez s’il faut générer le code accéléré ou de secours pour Sse2.ShiftRightLogical128BitLane, le compilateur détecte qu’une variable est passée au lieu d’une constante et décide prématurément de l’appel contre « intrinsifier ». À compter de .NET 9, le compilateur reconnaît plus de cas comme celui-ci et remplace l’argument variable par sa valeur constante, ce qui génère le code accéléré.

Pliage constant pour les opérations à virgule flottante et SIMD

Le repli constant est une optimisation existante dans le compilateur JIT. Le pliage constant fait référence au remplacement des expressions qui peuvent être calculées au moment de la compilation avec les constantes qu’ils évaluent, éliminant ainsi les calculs au moment de l’exécution. .NET 9 ajoute de nouvelles fonctionnalités de pliage constant :

  • Pour les opérations binaires à virgule flottante, où l’un des opérandes est une constante :
    • x + NaN est maintenant plié en NaN.
    • x * 1.0 est maintenant plié en x.
    • x + -0 est maintenant plié en x.
  • Pour les intrinsèques matérielles. Par exemple, en supposant qu’il s’agit x d’un Vector<T>:
    • x + Vector<T>.Zero est maintenant plié en x.
    • x & Vector<T>.Zero est maintenant plié en Vector<T>.Zero.
    • x & Vector<T>.AllBitsSet est maintenant plié en x.

Prise en charge de Arm64 SVE

.NET 9 introduit la prise en charge expérimentale de l’extension SVE (Scalable Vector Extension), un jeu d’instructions SIMD pour les processeurs ARM64. .NET a déjà pris en charge le jeu d’instructions NEON. Par conséquent, sur le matériel compatible AVEC NEON, vos applications peuvent tirer parti des registres vectoriels 128 bits. SVE prend en charge les longueurs de vecteur flexibles jusqu’à 2048 bits, déverrouillant davantage de traitement des données par instruction. Dans .NET 9, Vector<T> est large de 128 bits lors du ciblage de SVE, et le travail futur permettra la mise à l’échelle de sa largeur pour correspondre à la taille du registre vectoriel de l’ordinateur cible. Vous pouvez accélérer vos applications .NET sur du matériel compatible SVE à l’aide des nouvelles System.Runtime.Intrinsics.Arm.Sve API.

Note

La prise en charge de SVE dans .NET 9 est expérimentale. Les API sous System.Runtime.Intrinsics.Arm.Sve sont marquées avec ExperimentalAttribute, ce qui signifie qu’elles sont susceptibles de changer dans les versions ultérieures. En outre, le débogueur pas à pas et les points d’arrêt via le code généré par SVE peuvent ne pas fonctionner correctement, ce qui entraîne des blocages d’application ou une altération des données.

Allocation de pile d’objets pour les zones

Les types valeur, tels que int et struct, sont généralement alloués sur la pile au lieu du tas. Toutefois, pour activer différents modèles de code, ils sont fréquemment « boxés » dans des objets.

Considérez l’extrait de code suivant :

static bool Compare(object? x, object? y)
{
    if ((x == null) || (y == null))
    {
        return x == y;
    }

    return x.Equals(y);
}

public static int RunIt()
{
    bool result = Compare(3, 4);
    return result ? 0 : 100;
}

Compare est facilement écrit de sorte que si vous souhaitez comparer d’autres types, tels que des chaînes ou double des valeurs, vous pouvez réutiliser la même implémentation. Toutefois, dans cet exemple, il présente également l’inconvénient des performances d’exiger que tous les types de valeurs qui sont passés à celui-ci soient boxés.

Le code d’assembly x64 généré RunIt est le suivant :

push     rbx
sub      rsp, 32
mov      rcx, 0x7FFB9F8074D0      ; System.Int32
call     CORINFO_HELP_NEWSFAST
mov      rbx, rax
mov      dword ptr [rbx+0x08], 3
mov      rcx, 0x7FFB9F8074D0      ; System.Int32
call     CORINFO_HELP_NEWSFAST
mov      dword ptr [rax+0x08], 4
add      rbx, 8
mov      ecx, dword ptr [rbx]
cmp      ecx, dword ptr [rax+0x08]
sete     al
movzx    rax, al
xor      ecx, ecx
mov      edx, 100
test     eax, eax
mov      eax, edx
cmovne   eax, ecx
add      rsp, 32
pop      rbx
ret

Les appels à CORINFO_HELP_NEWSFAST sont les allocations de tas pour les arguments entiers boxed. Notez également qu’il n’y a pas d’appel à Compare; le compilateur a décidé de l’inliner dans RunIt. Cette inlining signifie que les boîtes ne sont jamais « échappées ». En d’autres termes, tout au long de l’exécution de Compare, il sait x et y sont en fait des entiers, et ils peuvent être sans risque les déboxer sans affecter la logique de comparaison.

À compter de .NET 9, le compilateur 64 bits alloue des zones non boucées sur la pile, ce qui déverrouille plusieurs autres optimisations. Dans cet exemple, le compilateur omet désormais les allocations de tas, mais, étant donné qu’il connaît x et y est 3 et 4, il peut également omettre le corps de ; le compilateur peut déterminer Compare est false au moment de x.Equals(y)la compilation. RunIt Il doit donc toujours retourner 100. Voici l’assembly mis à jour :

mov      eax, 100
ret