Leçon 1 : la mémoire — valeurs, références et spans
Sections
Réglages de lecture
Voix naturelle enregistrée pour ce cours, la même dans tous les navigateurs.
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
Code et termes techniques en anglais
Désactivé : la voix de la page lit tout, sans pause entre deux voix.
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
D’autres voix : Microsoft Edge propose des voix naturelles en ligne, et Windows (Paramètres › Heure et langue › Voix), macOS et Android en installent d’autres.
Vous connaissez la règle des livres de C# : les types valeur sont stockés en place, les types référence vivent sur le tas, et le boxing copie une valeur sur le tas. Cette leçon mesure chaque partie de cette règle. Elle compte les octets des valeurs et des objets, lit l’IL qu’émet le compilateur, et montre où le JIT de .NET 10 ignore ce que demande l’IL. Elle examine ensuite les outils que C# vous donne pour éviter les copies : in, les variables locales ref, ref struct et Span<T>, avec les erreurs du compilateur qui garantissent leur sûreté.
Les liens vers GA pointent vers le commit a826864 de GuitarAlchemist/ga ; les liens vers le runtime pointent vers le commit 4271d88 de dotnet/runtime, qui porte le tag v10.0.12.
Exécuter le programme de la leçon
Section intitulée « Exécuter le programme de la leçon »Le code se trouve dans code/csharp-advanced. check.sh clone GA à ce commit (seulement les trois projets que le programme référence), compile, exécute chaque leçon et compare la sortie avec expected/. Il vous faut le SDK .NET 10 et Git ; sous Windows, lancez-le depuis Git Bash.
bash code/csharp-advanced/check.sh # chaque leçon, l'IL et les erreurs du compilateurdotnet run --project code/csharp-advanced/Advanced -c Release -- l1 # cette leçon seulement, après check.shLes petites méthodes dont la leçon montre l’IL vivent dans leur propre projet, Snippets/Memory.cs, afin que leur IL ne bouge pas quand le reste du programme change. Mesurez toujours une build Release : une build Debug garde des variables locales supplémentaires et désactive la plupart des optimisations du JIT.
Quelle est la taille d’une valeur, et celle d’un objet ?
Section intitulée « Quelle est la taille d’une valeur, et celle d’un objet ? »Deux nombres comptent. Unsafe.SizeOf<T>() donne la taille en place : ce qu’occupe un champ ou un élément de tableau de type T. Pour un type référence, c’est la taille de la référence, 8 octets dans un processus 64 bits. La taille sur le tas est ce que coûte une allocation, et le programme la mesure avec GC.GetAllocatedBytesForCurrentThread() autour d’un deuxième appel du code qui alloue, afin que le JIT et les constructeurs statiques aient déjà fait leur travail (Report.cs#L17-L24).
== Inline size (Unsafe.SizeOf) and heap size of one allocation, in bytestype inline heap what the heap bytes areint 4 24 a boxed intlong 8 24 a boxed longdecimal 16 32 a boxed decimalGuid 16 32 a boxed GuidPoint (2 ints) 8 24 a boxed PointPadded (byte, long) 16 32 a boxed Padded(byte, long) 16 32 a boxed tupleobject 8 24 new object()Node (class, 1 int) 8 24 new Node(1)string 8 32 a 5-char stringint[] 8 24 an empty int[]int[] 8 64 an int[10]GA PitchClass 4 24 a boxed PitchClassGA PitchClassSetId 4 24 a boxed PitchClassSetIdChaque objet du tas 64 bits commence par deux mots de la taille d’un pointeur : l’en-tête de l’objet (utilisé pour les verrous et les codes de hachage) et le pointeur vers la table des méthodes, qui identifie le type. Viennent ensuite les champs, et le total est arrondi au multiple de 8 supérieur. Le minimum est de 24 octets, même pour new object(), qui n’a aucun champ.
- Un
intboxé fait 8 + 8 + 4 = 20 octets, arrondis à 24. Unlongboxé tient exactement dans 24 octets. Paddedcontient unbyteet unlong, mais occupe 16 octets en place : lelongdoit commencer sur une frontière de 8 octets, donc la disposition séquentielle par défaut du compilateur laisse 7 octets de remplissage après lebyte.- Un tableau ajoute sa longueur (4 octets, complétés à 8) à l’en-tête : 24 octets quand il est vide, et 24 + 10 × 4 = 64 pour dix
int. - Une chaîne stocke sa longueur, ses caractères UTF-16 et un caractère nul final : 8 + 8 + 4 + 5 × 2 + 2 = 32 octets pour cinq caractères.
GA modélise la musique avec de petits objets valeur. PitchClass est un readonly record struct qui enveloppe un seul int, et PitchClassSetId aussi : 4 octets chacun, aussi peu coûteux que l’int lui-même, tant qu’ils ne sont pas boxés.
Le boxing : dans l’IL, et après le JIT
Section intitulée « Le boxing : dans l’IL, et après le JIT »Le boxing convertit une valeur en object ou en interface : le runtime alloue un objet et y copie la valeur (Boxing et unboxing). Le compilateur émet une instruction box partout où cela se produit. Voici cinq petites méthodes, et l’IL de trois d’entre elles, désassemblé avec ilspycmd -il par check.sh (expected/snippets-il.txt).
public static int ToObjectAndBack(int value){ object boxed = value; return (int)boxed;}
public static bool ThroughInterface<T>(T left, T right) where T : struct, IEquatable<T>{ IEquatable<T> equatable = left; return equatable.Equals(right);}
public static bool ThroughConstraint<T>(T left, T right) where T : IEquatable<T> => left.Equals(right);
public static string Format(int value) => string.Format("{0}", value);
public static string Interpolate(int value) => $"{value}"; IL_0000: ldarg.0 IL_0001: box !!T IL_0006: ldarg.1 IL_0007: callvirt instance bool class [System.Runtime]System.IEquatable`1<!!T>::Equals(!0) IL_000c: ret } // end of method Boxing::ThroughInterface
IL_0000: ldarga.s left IL_0002: ldarg.1 IL_0003: constrained. !!T IL_0009: callvirt instance bool class [System.Runtime]System.IEquatable`1<!!T>::Equals(!0) IL_000e: ret } // end of method Boxing::ThroughConstraint
IL_0000: ldstr "{0}" IL_0005: ldarg.0 IL_0006: box [System.Runtime]System.Int32 IL_000b: call string [System.Runtime]System.String::Format(string, object) IL_0010: ret } // end of method Boxing::Format- Convertir une valeur en interface produit un
box, même quand l’interface est générique. - Appeler la même méthode à travers une contrainte générique n’en produit pas : le préfixe
constrained.permet au runtime d’appelerEqualsdirectement sur la valeur. C’est pourquoi le code générique surT : IEquatable<T>ne boxe pas, et pourquoi GA peut déclarer le comportement de ses objets valeur dans des interfaces à membres abstraits statiques commeIStaticValueObjectList<TSelf>sans en payer le prix. string.Format(string, object)prend unobject: l’intest boxé. Une chaîne interpolée est compilée en unDefaultInterpolatedStringHandler, dont la méthodeAppendFormatted<T>est générique : aucunbox.ToObjectAndBackcontient unboxsuivi d’ununbox.anydu même type.
Voyons maintenant les octets qu’alloue chaque appel. Le programme produit le tableau deux fois : une fois normalement, où ces méthodes s’exécutent dans le premier niveau du JIT, non optimisé ; et une fois avec DOTNET_TieredCompilation=0, où chaque méthode est compilée d’emblée avec toutes les optimisations (Lesson1.cs#L103-L116).
== Boxing: bytes allocated by the second call (tiered JIT, first tier)Boxing.ToObjectAndBack(42) 0Escape.BoxAndHash(42) 24Boxing.ThroughInterface(pc, pc) 24Boxing.ThroughConstraint(pc, pc) 0Boxing.Format(12345) 56Boxing.Interpolate(12345) 32== Boxing: bytes allocated by the second call (DOTNET_TieredCompilation=0)Boxing.ToObjectAndBack(42) 0Escape.BoxAndHash(42) 0Boxing.ThroughInterface(pc, pc) 0Boxing.ThroughConstraint(pc, pc) 0Boxing.Format(12345) 56Boxing.Interpolate(12345) 32Il s’est passé trois choses que l’IL ne dit pas.
ToObjectAndBackn’alloue jamais, même au premier niveau : le JIT reconnaît unboximmédiatement suivi d’ununbox.anyet supprime les deux.- Avec l’optimisation complète,
BoxAndHash(qui boxe, puis appelleGetHashCode()sur la boîte) etThroughInterfacen’allouent rien. Le JIT a prouvé que la boîte ne s’échappe pas de la méthode, et l’a placée sur la pile. L’allocation des boîtes sur la pile est arrivée avec .NET 9, et .NET 10 étend l’analyse d’échappement aux petits tableaux, aux champs de structs et aux délégués. Lors d’une exécution normale, une méthode appelée assez souvent est recompilée avec ces optimisations après quelques dizaines d’appels ; la leçon 4 mesure la compilation par niveaux. Formatalloue toujours 56 octets : 24 pour l’intboxé, 32 pour la chaîne"12345". La boîte est passée àstring.Format, une grande méthode que le JIT n’intègre pas dans l’appelant, donc elle s’échappe.Interpolaten’alloue que la chaîne.
Ainsi, « le boxing alloue » est vrai de l’IL, et seulement parfois du code machine. L’IL vous dit où regarder ; une mesure vous dit ce que cela coûte.
in et ref : les copies que vous ne voyez pas
Section intitulée « in et ref : les copies que vous ne voyez pas »Une struct passée par valeur est copiée. Les paramètres ref, in et ref readonly passent à la place une référence vers elle. in promet à l’appelant que la méthode ne modifiera pas l’argument. Pour tenir cette promesse quand la struct est mutable, le compilateur doit supposer que tout appel de méthode peut la modifier, et il appelle donc la méthode sur une copie cachée.
public struct Counter{ public int Value;
public void Increment() => Value++;}
public static class DefensiveCopy{ public static int ByIn(in Counter counter) { counter.Increment(); return counter.Value; }
public static int ByRef(ref Counter counter) { counter.Increment(); return counter.Value; }}== in and ref: a method called on an in parameter runs on a copyByIn returns 0, counter.Value is now 0ByRef returns 1, counter.Value is now 1Aucun avertissement, aucune erreur : l’incrément est perdu en silence. L’IL de ByIn montre la copie, ldobj dans la variable locale 0, puis l’appel sur l’adresse de cette variable :
IL_0000: ldarg.0 IL_0001: ldobj Snippets.Counter IL_0006: stloc.0 IL_0007: ldloca.s 0 IL_0009: call instance void Snippets.Counter::Increment() IL_000e: ldarg.0 IL_000f: ldfld int32 Snippets.Counter::Value IL_0014: ret } // end of method DefensiveCopy::ByInLa même copie défensive se produit quand vous appelez une méthode sur un champ readonly dont le type est une struct mutable. La solution consiste à rendre la struct, ou au moins la méthode, readonly : le compilateur sait alors que l’appel ne peut pas modifier la valeur et passe directement la référence. Les paramètres ref readonly (C# 12) se comportent comme in à l’intérieur de la méthode, mais demandent à l’appelant de passer une variable avec ref ou in, et produisent un avertissement quand il passe une valeur temporaire : utilisez-les pour les API qui ont besoin d’une référence vers un emplacement existant, pas simplement pour éviter une copie. Affecter un champ d’un paramètre in est une erreur de compilation :
public static void Reset(in Counter counter) => counter.Value = 0;l1_in_assign.cs(9,53): error CS8332: Cannot assign to a member of variable 'counter' or use it as the right hand side of a ref assignment because it is a readonly variableChaque extrait rejeté du cours est compilé seul par CompileFail/Program.cs, qui utilise Roslyn 5.0.0, le compilateur du SDK .NET 10.0.1xx ; les messages sont identiques à ceux de dotnet build (expected/compile-fail.txt).
Variables locales et retours ref
Section intitulée « Variables locales et retours ref »Une variable locale ref est un alias vers un emplacement de stockage : un élément de tableau, un champ, une entrée de dictionnaire. CollectionsMarshal.GetValueRefOrAddDefault renvoie une référence vers la valeur d’une entrée de Dictionary, en ajoutant l’entrée si elle manque. Incrémenter un compteur ne demande alors qu’une recherche par hachage au lieu de deux (TryGetValue, puis l’affectation par l’indexeur). Le programme compte les 4 096 ensembles de classes de hauteurs de GA par nombre de notes :
var byCardinality = new Dictionary<int, int>();foreach (var id in PitchClassSetId.Items){ ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(byCardinality, id.Cardinality, out _); count++;}== ref locals: one dictionary lookup per update (CollectionsMarshal)sets by number of notes: 1 12 66 220 495 792 924 792 495 220 66 12 1Ce sont les coefficients binomiaux C(12, k) : un ensemble vide, 12 notes seules, 66 intervalles, 220 ensembles de trois notes. La référence n’est valide que tant que le dictionnaire ne change pas : ajouter une autre clé peut le redimensionner et déplacer les entrées, c’est pourquoi la méthode se trouve dans CollectionsMarshal et non sur Dictionary lui-même.
Le compilateur suit l’endroit vers lequel pointe une référence. Renvoyer une référence vers une variable locale, dont le stockage disparaît avec le cadre de pile de la méthode, est refusé :
public static ref int Next(){ var count = 0; return ref count;}l1_ref_to_local.cs(7,20): error CS8168: Cannot return local 'count' by reference because it is not a ref localref struct, Span<T> et stackalloc
Section intitulée « ref struct, Span<T> et stackalloc »Span<T> est une référence plus une longueur : une fenêtre sur un tableau, une chaîne, de la mémoire native ou la pile. Pour être sûr, il ne doit jamais survivre à la mémoire vers laquelle il pointe, c’est donc une ref struct : une struct qui ne peut vivre que sur la pile. Le compilateur l’impose au moyen d’une famille d’erreurs. Une ref struct ne peut pas être un champ d’une classe :
public class FretBuffer{ private Span<int> _frets; // un Span ne peut vivre que sur la pile}l1_ref_struct_field.cs(4,13): error CS8345: Field or auto-implemented property cannot be of type 'Span<int>' unless it is an instance member of a ref struct.Elle ne peut pas être capturée par une lambda, qui la stockerait dans un objet de fermeture sur le tas :
public static Func<int> Sum(Span<int> frets) => () => frets[0] + frets[1];l1_span_lambda.cs(4,59): error CS9108: Cannot use parameter 'frets' that has ref-like type inside an anonymous method, lambda expression, query expression, or local functionl1_span_lambda.cs(4,70): error CS9108: Cannot use parameter 'frets' that has ref-like type inside an anonymous method, lambda expression, query expression, or local functionElle ne peut pas être l’argument de type d’un type générique qui ne l’autorise pas explicitement. Depuis C# 13, un paramètre générique peut déclarer allows ref struct ; List<T> ne le fait pas, parce qu’il stocke ses éléments dans un tableau :
public static List<Span<int>> Shapes = [];l1_span_type_argument.cs(4,35): error CS9244: The type 'Span<int>' may not be a ref struct or a type parameter allowing ref structs in order to use it as parameter 'T' in the generic type or method 'List<T>'Et la mémoire obtenue par stackalloc ne peut pas quitter la méthode qui l’a allouée :
public static Span<int> Empty(){ Span<int> frets = stackalloc int[6]; return frets; // la mémoire disparaît quand la méthode se termine}l1_stackalloc_escape.cs(7,16): error CS8352: Cannot use variable 'frets' in this context because it may expose referenced variables outside of their declaration scopeQuand les données doivent survivre au cadre de pile, par-delà un await ou dans un champ, utilisez Memory<T>, une struct ordinaire qui peut être stockée n’importe où et transformée en Span<T> là où le travail se fait.
Dans le cadre de ces règles, les spans suppriment des allocations. Un voicing de guitare s’écrit souvent sous la forme de six cases, x 3 2 0 1 0 pour un accord de do ouvert. La première version découpe la chaîne ; la seconde la tranche avec MemoryExtensions.Split, qui renvoie des plages, analyse chaque tranche avec int.Parse(ReadOnlySpan<char>), et stocke les cases dans six int sur la pile (Lesson1.cs#L126-L155).
public static int SumWithSplit(string voicing){ var sum = 0; foreach (var part in voicing.Split(' ')) { if (part != "x") { sum += int.Parse(part); } } return sum;}
public static int SumWithSpans(string voicing){ Span<int> frets = stackalloc int[6]; var count = 0; var text = voicing.AsSpan(); foreach (var range in text.Split(' ')) { var part = text[range]; frets[count++] = part is "x" ? -1 : int.Parse(part); } var sum = 0; foreach (var fret in frets[..count]) { if (fret > 0) sum += fret; } return sum;}== Parsing a voicing: string.Split versus spanssplit: 6 frets, 216 bytesspans: 6 frets, 0 bytesLes 216 octets sont le string[] de six éléments (24 + 6 × 8 = 72 octets) et six chaînes d’un caractère, de 24 octets chacune. Pour un seul voicing, ce n’est rien ; pour les millions de voicings qu’énumère une recherche d’accords, c’est du travail pour le ramasse-miettes, que la leçon 2 mesure.
Une copie par appel dans GA
Section intitulée « Une copie par appel dans GA »Les objets valeur de GA exposent leurs valeurs possibles sous forme de span. Pour PitchClass, ItemsSpan renvoie un tableau mis en cache (ValueObjectCache.cs#L45-L49). PitchClassSetId a sa propre implémentation (PitchClassSetId.cs#L59, #L70-L71) :
public static ReadOnlySpan<PitchClassSetId> ItemsSpan => Items is PitchClassSetId[] arr ? arr : [.. Items];
public static IReadOnlyCollection<PitchClassSetId> Items { get; } = [.. Enumerable.Range(_minValue, _maxValue - _minValue + 1).Select(i => new PitchClassSetId(i))];L’intention est claire : renvoyer le tableau sans le copier. Mais Items est initialisée avec une expression de collection dont la cible est l’interface IReadOnlyCollection<T>, et pour cette cible le compilateur est libre de construire un type à lui (expressions de collection, traduction vers une interface non mutable). Le test Items is PitchClassSetId[] est donc toujours faux, et chaque lecture d’ItemsSpan copie 4 096 identifiants dans un nouveau tableau :
== GA: ItemsSpan of two value objectsPitchClass.ItemsSpan: 12 items, 0 bytesPitchClassSetId.ItemsSpan: 4096 items, 16408 bytesPitchClassSetId.Items is <>z__ReadOnlyList`1Ces 16 408 octets, ce sont 24 octets d’en-tête de tableau plus 4 096 × 4. Le code compile, s’exécute et renvoie les bonnes valeurs ; seule une mesure en révèle le coût. La correction fait l’objet de l’exercice 3.
Si vous connaissez Spring et Reactor
Section intitulée « Si vous connaissez Spring et Reactor »Cette leçon n’a presque pas d’équivalent Java, et c’est justement ce qu’elle apprend. Sur la JVM, tout type que vous déclarez est un type référence : PitchClass, qui tient ici sur 4 octets en ligne, y serait un objet avec un en-tête, et une liste de PitchClass une liste de références. C# vous donne des outils pour garder les valeurs hors du tas ; Java demande à son ramasse-miettes de les faire disparaître vite, ce que compare la leçon 2.
| C# | Java, Spring et Reactor |
|---|---|
struct, readonly record struct : 4 octets, en ligne, sans en-tête |
aucun type valeur défini par l’utilisateur ; un record est un objet ordinaire du tas. Les objets valeur du projet Valhalla sont une préversion visée pour le JDK 28 (JEP 401), donc absents de tout JDK publié |
le boxing est une instruction box, que le JIT supprime parfois |
l’autoboxing appelle Integer.valueOf (restrictions des génériques) ; l’analyse d’échappement de HotSpot, -XX:+DoEscapeAnalysis, activée par défaut, peut supprimer l’allocation |
les génériques sont réifiés : un PitchClass[] contient des valeurs de 4 octets |
les génériques sont effacés (JLS §4.6) : une List<Integer> contient des références vers des objets boxés, et List<int> ne compile pas |
Span<T> sur un tableau, une chaîne, de la mémoire native ou la pile |
ByteBuffer, le ByteBuf de Netty, le DataBuffer de WebFlux : des objets, et aucun n’est une fenêtre sur une String |
stackalloc |
rien d’équivalent. ByteBuffer.allocateDirect alloue hors du tas, et sa Javadoc la réserve « primarily for large, long-lived buffers » |
| CS8345, CS8352 et les autres erreurs de sûreté des références : le compilateur prouve qu’un span ne survit pas à sa mémoire | un comptage de références vérifié à l’exécution : release(), IllegalReferenceCountException, et un détecteur de fuites qui échantillonne environ 1 % des allocations |
Memory<T> pour les données qui traversent un await |
un tampon du pool retenu au-delà d’un opérateur, libéré par celui qui le lit en dernier |
une copie défensive silencieuse quand on appelle une méthode sur un paramètre in |
ne se pose pas : les objets sont toujours passés par référence |
La ligne qui coûte du temps de débogage, c’est la propriété. Un Span<T> ne possède rien, et le compilateur rejette le code qui le laisserait survivre à sa mémoire. Un PooledDataBuffer démarre à un compteur de références de 1, que retain() et release() font varier, et la documentation de Spring est explicite : « special care must be taken to ensure buffers are released since they may be pooled », avec une règle par cas — libérer chaque tampon lu, et ajouter doOnDiscard(DataBuffer.class, DataBufferUtils::release) quand un opérateur peut écarter des éléments. Cette charge ne vous revient que si vous manipulez vous-même des DataBuffer : décodez vers une String ou un record, et le codec a déjà libéré. Les deux plateformes résolvent le même problème, l’une par un système de types, l’autre par une discipline et un détecteur de fuites.
L’ItemsSpan de GA a aussi sa forme Java. Une propriété qui renvoie collection.toArray() copie à chaque lecture, renvoie les bonnes valeurs, et aucun test ne le remarque ; la mesure qui a trouvé les 16 408 octets ici est celle qu’il faut lancer là-bas.
Exercices
Section intitulée « Exercices »- Prédisez
Unsafe.SizeOfdu tuple(bool, int, bool), d’une struct avec les champsbool Muted; int Fret; bool Barre;dans cet ordre, et de la même struct marquée[StructLayout(LayoutKind.Auto)]. - Faites détecter par le compilateur l’incrément perdu de
ByIn: que changez-vous dansCounter, et que dit alors le compilateur à propos d’Increment? - Réécrivez
PitchClassSetId.ItemsSpanpour qu’elle n’alloue pas, et vérifiez qu’elle renvoie les mêmes identifiants.
Solutions
-
La struct dans l’ordre de déclaration occupe 12 octets :
bool(1 octet, plus 3 de remplissage),int(4),bool(1, plus 3 de remplissage), parce que la taille d’une struct est un multiple de l’alignement de son plus grand champ. AvecLayoutKind.Auto, le runtime peut réordonner les champs :int,bool,bool, 2 de remplissage, soit 8 octets.ValueTupleest déclaré avecLayoutKind.Auto, donc le tuple occupe lui aussi 8 octets. La disposition séquentielle est la valeur par défaut des structs en C# parce qu’elle correspond au code natif lors de l’interop ; pour une struct qui ne passe jamais au code natif,Autopeut faire gagner de la place.1. (bool, int, bool) 8 bytes, SequentialFlags 12, AutoFlags 8 -
Marquez
Incrementcomme membrereadonly, ou la struct entière commereadonly struct. Le compilateur refuse alors l’incrément à l’intérieur de la méthode, et l’appel sur un paramètreinn’a plus besoin de copie défensive. Pour garder une méthode qui modifie la valeur, prenez plutôt le paramètre parref.public readonly void Increment() => Value++;l1_readonly_member.cs(6,41): error CS1604: Cannot assign to 'Value' because it is read-only -
Stockez une fois pour toutes les identifiants dans un tableau, et renvoyez le tableau sous forme de span : la conversion de
T[]enReadOnlySpan<T>ne copie pas (Lesson1.cs#L94-L100). Dans GA même, déclarerItemsavec un tableau concret, ou passer parValueObjectUtils<PitchClassSetId>.ItemsSpancomme les autres objets valeur, aurait le même effet.private static readonly PitchClassSetId[] Items = [.. PitchClassSetId.Items];public static ReadOnlySpan<PitchClassSetId> ItemsSpan => Items;3. cached ItemsSpan: 4096 items, 0 bytes, same ids True
À retenir
Section intitulée « À retenir »- Un objet 64 bits coûte au moins 24 octets : l’en-tête, le pointeur vers la table des méthodes, puis les champs, le tout arrondi à un multiple de 8.
Unsafe.SizeOf<T>()donne la taille en place ;GC.GetAllocatedBytesForCurrentThread()autour d’un appel déjà rodé donne le coût sur le tas. - L’IL montre le boxing sous la forme d’un
box. Les contraintes génériques appellent la méthode viaconstrained.sans boxing, et les chaînes interpolées se formatent sans boxing. - Le JIT peut supprimer une boîte que l’IL demande : toujours pour un
boxsuivi d’ununbox.any, et, dans le code optimisé, chaque fois que la boîte ne s’échappe pas de la méthode. Mesurez le code optimisé, pas l’IL. - Appeler une méthode sur un paramètre
inou sur un champreadonlyd’une struct mutable l’exécute sur une copie silencieuse. Rendez la struct ou la méthodereadonly. Span<T>etstackallocpermettent d’analyser et de découper sans allouer. Les erreurs de sûreté des références du compilateur (CS8345, CS8352, CS9108, CS9244, CS8168) sont ce qui les empêche de pointer vers une mémoire qui n’existe plus ;Memory<T>sert aux données qui doivent survivre au cadre de pile.- Une expression de collection affectée à une interface n’est pas un tableau. C’est pour cette raison que
PitchClassSetId.ItemsSpande GA copie 16 Ko à chaque appel.
- Microsoft Learn :
Unsafe.SizeOf, Boxing et unboxing, Paramètres de méthode, Types ref struct,stackalloc, Recommandations d’utilisation de Memory et Span, Expressions de collection. - Nouveautés du runtime : .NET 9, allocation sur la pile des objets boxés, .NET 10, allocation sur la pile.
- ECMA-335, partition III, pour
box,unbox.anyet le préfixeconstrained.. - La proposition du langage C# pour les expressions de collection.