Structure exclusivement stockée dans la pile: “ref struct” (C# 7, C# 8.0)

Cet article fait partie d’une série d’articles sur les apports fonctionnels de C# 7 (i.e. C# 7.0/7.1/7.2/7.3) et C# 8.0.

Les objets de type référence sont stockés dans le tas managé (i.e. managed heap) et les objets de type valeur sont le plus souvent stockés sur la pile (i.e. stack). Quand on manipule des objets de type référence, le plus souvent on manipule des références vers ces objets. A l’opposé, quand on manipule des objets de type valeur, des copies par valeur de ces objets peuvent être effectuées suivant la façon dont on les manipule.

L’utilisation d’objets dans la pile peut être plus performante que dans le tas managé à condition que les manipulations de ces objets n’entraînent pas trop de copies par valeur. Certains éléments de syntaxe ont été rajoutés en C# 7.2 de façon à limiter les copies d’objets de type valeur et ainsi garantir une utilisation plus optimale de ces objets:

  • in: permet de définir des arguments de fonction qui seront copiés par référence en empêchant leur modification dans le corps de la fonction (cf. mot-clé in à partir de C# 7.2).
  • readonly struct pour définir des structures et garantir qu’elles sont immutables,
  • ref struct: pour déclarer des structures qui seront stockées exclusivement sur la pile.

Tous ces éléments servent au compilateur pour effectuer des optimisations pour améliorer la vitesse d’exécution.

Pour optimiser l’utilisation des objets de type valeur dans la pile (i.e. stack), il faut:

  • Eviter d’effectuer des copies par valeur de ces objets et privilégier les manipulations par référence.
  • Utiliser des objets immutables ce qui permet au compilateur d’effectuer des optimisations.
Utilisation de variables temporaires avec in

Utiliser des copies par référence ou rendre les objets immutables sont des modifications qui ne suffisent pas pour optimiser les performances si elles sont appliquées l’une sans l’autre.

Par exemple utiliser des structures dont les membres sont rendus immutables en utilisant readonly peut, en réalité, dégrader les performances:

public struct ImmutableStruct  
{  
  private readonly int innerMember;  

  public int ReadOnlyMember => this.innerMember;  
}  

public class StructWrapper  
{  
  private readonly ImmutableStruct innerStruct;  

  // ...  
}  

En effet pour prendre en compte l’aspect immutable des membres de la structure, pour chaque utilisation d’un membre, le compilateur utilise une variable temporaire qui sera une copie du membre à utiliser. Ainsi à chaque utilisation d’un membre de la structure, une copie par valeur sera effectuée vers une variable temporaire. Cette opération dégrade les performances et peut doubler le temps d’exécution par rapport à l’absence du mot clé readonly:

public struct MutableStruct  
{  
  public int innerMember;  
}  

public class StructWrapper  
{  
  private MutableStruct innerStruct;  

  // ...  
}  

Le même mécanisme est appliqué par le compilateur quand on utilise in pour des arguments de fonctions qui sont des structures:

public void UseStruct(in ImmutableStruct)  
{...}  

Pour empêcher au compilateur d’effectuer ces copies, il faut que la structure soit immutable par construction en utilisant readonly struct.

readonly struct

C# 7.2

Historiquement le mot-clé readonly pouvait être utilisé pour indiquer qu’un membre d’une classe ou d’une structure ne peut être initialisé que par un initializer (avant l’exécution du constructeur) ou par le constructeur.

A partir de C# 7.2, le mot-clé readonly peut être placé devant struct de façon à indiquer au compilateur que la structure doit être immutable. Par suite le compilateur vérifiera que les membres de la structure ne peuvent pas être modifiés:

  • Une propriété ne pourra pas avoir d’accesseurs en écriture:
    public readonly struct MyStruct  
    {  
      public int WritableProp { get; set; } // ERREUR  
    
      public int ReadOnlyProp { get; } // OK  
    }
    
  • Les variables membres publiques doivent utiliser le mot-clé readonly:
    public readonly struct MyStruct  
    {  
      public int WritableMember; // ERREUR  
    
      public readonly int ReadOnlyMember; // OK  
    }
    
  • La déclaration d’évènements dans la structure n’est pas autorisée:
    public readonly struct MyStruct  
    {  
      public event EventHandler Event; // ERREUR  
    }
    

Ainsi la syntaxe permet de garantir que la structure est immutable.

ref struct

C# 7.2

ref peut être utilisé quand on déclare un objet struct pour indiquer qu’une instance de la structure ne peut se trouver que dans la pile et ne pourra pas correspondre à une allocation dans le tas managé, par exemple:

ref struct StackOnlyStruct  
{ ... }  

Une structure étant un objet de type valeur, elle est, le plus souvent, stockée dans le pile (i.e. stack). Toutefois dans certains cas, une instance provenant d’une structure peut être stockée ailleurs:

  • Si une structure est déclarée en tant qu’objet statique, l’instance sera allouée dans un tas particulier (loader heap ou high frequency heap).
  • Si la structure est un membre d’un objet de type référence alors la structure sera stockée dans le tas managé (i.e. managed heap):
    public class StructWrapper  
    {  
      public CustomStruct InnerStruct { get; set; }  
    }
    

    avec:

    struct CustomStruct { ... }
    

    Dans le cas du boxing, une structure peut aussi être amenée à être stockée dans le tas managé:

    CustomStruct customStruct = new CustomStruct();  
    object structAfterBoxing = customStruct; // Boxing  
    

Ainsi suivant les utilisations, une structure peut faire l’objet d’une allocation dans le tas managé. Or dans certains cas, il peut être nécessaire d’indiquer au compilateur que les instances de la structure doivent exclusivement se trouver dans la pile. ref struct vise à l’assurer par construction dans le cas contraire une erreur de compilation sera émise:

ref struct CustomStruct {}  

// ...  
var customStruct = new CustomStruct();  
object boxedStruct = (object)customStruct; // ERROR: Cannot concert type 'CustomStruct' to 'object'  

Pour garantir que la structure ne sera stockée que dans le pile, d’autres restrictions s’ajoutent si la structure est déclarée avec ref struct. Ainsi certains cas d’utilisation ne sont pas possibles pour ce type de structure:

  • Elément d’un tableau: CustomStruct[].
  • Membres d’une classe.
  • Satisfaire une interface (pourrait entraîner un boxing).
  • Argument de type générique par exemple pour une méthode:
    public void UseStruct<T>(T argument) {}  
    
    // ...  
    var customStruct = new CustomStruct();  
    UseStruct<CustomStruct>(customStruct); // ERROR: The type 'CustomStruct' may not be used as a type argument  
    
  • Argument d’une expression lambda ou d’une fonction locale:
    Func<CustomStruct> lambda = () => new CustomStruct(); // ERROR  
    
  • Utilisation dans une méthode async:
    Task UseStruct()  
    {  
      var customStruct = new CustomStruct();  
      return Task.FromResult(0); // OK pas d'erreur  
    }  
    

    Mais:

    async Task UseStruct()  
    {  
      var customStruct = new CustomStruct(); // ERROR: parameters or locals of type 'CustomStruct' cannot be declared in async methods or lambda expressions.  
      await Task.FromResult(0);  
    }
    
  • Utilisation dans un itérateur.

L’utilisation d’une structure déclarée avec ref struct est seulement réservée:

  • Au paramètre d’une méthode;
  • Au type de retour d’une méthode
  • A une variable locale.

D’autre part, les accès au ref struct ne peut se faire qu’à partir du thread qui a créé l’instance. Il n’est pas possible de passer des adresses de ref struct d’un thread à l’autre.

L’utilisation de ref struct pour déclarer les structures réservées exclusivement à la pile prête beaucoup à confusion car elle laisse penser qu’il s’agirait de structure de type référence (ce type de structure existe en C++/CLI). ref est utilisé car il fait référence aux objets de type ByReference (byref ou byref-like types) qui sont liés aux pointeurs managés (i.e. managed pointers).

Enfin, on pourrait se poser la question de savoir quelle pourrait être le cas d’utilisation de ce type de structure. Une utilisation est de les utiliser avec des pointeurs managés (i.e. managed pointer) qui sont des objets exclusivement stockés sur la pile.

Managed pointer

Les pointeurs managés (i.e. managed pointer) sont des pointeurs particuliers utilisés en .NET de façon sous-jacente mais non exposés aux développeurs en C#. Il s’agit de pointeurs permettant de pointer vers des objets managés, des objets non managés ou des objets se trouvant sur la pile. Ils sont différents des pointeurs natifs car ils permettent de pointer sur les objets managés. De la même façon, ils ne peuvent pas être considérés comme des références car ils peuvent pointer vers des objets non managés et surtout, ils peuvent pointer à l’intérieur d’objets. Durant l’exécution normale, les pointeurs managés sont utilisés pour lire ou écrire dans les emplacements mémoires vers lesquels ils pointent.

Une spécificité importante qui découle des caractéristiques de ces pointeurs les différenciant des références est qu’ils peuvent pointer vers des objets se trouvant à l’intérieur d’objets. Ainsi lorsque des objets sont passés en argument par référence en utilisant ref, ce sont des pointeurs managés qui sont utilisés (ref n’est pas un pointeur managé mais il wrappe un pointeur managé). Les différentes manipulations possibles de l’utilisation d’objets de type valeur par référence découlent directement des caractéristiques du pointeur managé:

Les pointeurs managés étant capable de pointer vers l’intérieur d’un objet, ils sont aussi appelés interior pointer. Par exemple, c’est sous cette appellation qui sont disponibles en C++/CLI: interior_ptr.

Les interior pointers sont exclusivement stockés dans la pile et non dans le tas managé pour 2 raisons:

  • Les interior pointers peuvent pointer vers un objet de la pile: si un interior pointer pointant vers un objet de la pile était stocké dans le tas managé, comment savoir quand le pointeur n’est plus valide quand l’objet pointé ne se trouve plus dans la stack frame ? Dans le cas où l’interior pointer est stocké dans la pile, par construction le principe de fonctionnement des stack frames permet de gérer la durée de vie du pointeur en accord avec celle de l’objet vers lequel il pointe.
  • Les interior pointers peuvent pointer à l’intérieur d’objets managés: c’est ce cas de figure qui pose le plus de problème. Lorsqu’un objet managé est déplacé en mémoire par le garbage collector (GC) lors de la phase de compactage, il faut répercuter ce changement dans le pointeur managé de façon à ce qu’il pointe vers le nouvel emplacement mémoire. Ce mécanisme est effectué par le GC avant l’étape de compactage et il est quasiment le même pour les références.

    La différence entre le changement des adresses des références et des interior pointers est que ces derniers pointent vers l’intérieur d’objets et il n’est pas direct de savoir qu’elle est l’objet parent de l’objet pointé par l’interior pointer (car les objets ne sont pas stockés de façon continue en mémoire). Cette étape nécessite de parcourir le graphe des objets en utilisant le mécanisme d’arbre “bricks and plugs”(1) (i.e. bricks and plugs tree mechanism). Ainsi pour un interior pointer donné, il sera possible de déterminer quel est l’objet parent de l’objet qui est pointé et marquer l’objet parent comme étant actif lors de l’étape de marquage du GC.

    Il n’est pas non plus direct pour le GC de connaître quel est l’objet parent d’un interior pointer. Si l’interior pointer était stocké dans le tas managé, trouver l’objet parent representerait un coût non négligeable puisqu’il faudrait parcourir l’arbre des objets de la même façon que pour le mécanisme d’arbre “bricks and plugs” après la phase de marquage du GC. Pour éviter ce parcours supplémentaire, le choix a été fait de limiter le stockage des interior pointers à la pile. Ainsi il est plus facile de modifier les adresses vers lesquelles pointent les interior pointers après la phase de compactage du GC.

Phases du garbage collector

Lorsque le garbage collector s’exécute, il stoppe l’exécution et applique 3 phases successives:

  1. Marquage (mark): au début de son exécution le GC considère tous les objets comme étant des objets à supprimer. Le but de cette étape est de marquer les objets utilisés. Le GC commence par référencer tous les objets se trouvant sur la pile, dans les registres et les objets statiques, ensuite il parcours toutes les références ou pointeurs se trouvant dans ces objets. Tous les objets atteignables au moyen d’une réference ou d’un pointeur sont considérés comme actif. A la fin de cette étape, le GC possède une cartographie des objets du processus sous la forme d’un graphe d’objets.
  2. Collecte (collect ou sweep): cette étape consiste à supprimer tous les objets qui ne sont pas actifs c’est-à-dire qui sont inatteignables. Après suppression des objets, la mémoire risque d’être morcellée ce qui peut rendre les nouvelles allocations d’objets plus difficiles. C’est la raison pour laquelle, à la fin de l’étape de collecte, le GC détermine quels sont les objets qu’il devra déplacer en mémoire par copie de façon à limiter le morcellement de la mémoire par les objets encore actifs. A la fin de l’étape de collecte, le GC corrige les adresses des références et de pointeurs managés en affectant les nouvelles adresses des objets après déplacement en mémoire.
  3. Compactage (compact): cette étape consiste à déplacer les objets en mémoire pour éviter le morcellement et optimiser les nouvelles allocations d’objets.

Pour toutes ces raisons les pointeurs managés sont stockés sur la pile. Par suite les objets contenant des pointeurs managés doivent aussi être stockés sur la pile. C’est le cas de l’objet Span<T> qui est apparu avec .NET Core 2.1.

Span<T>

C# 7.2 / .NET Core 2.1

Span<T> est un objet apparu avec .NET Core 2.1 (il n’est pas directement disponible avec le framework .NET). L’utilisation de cet objet ainsi que ReadOnlySpan<T> justifie de devoir construire des structures avec ref struct. En effet Span<T> est une ref struct et ne peut être utilisée que sur la pile ou dans une autre structure ref struct. Par exemple, si on essaie de l’utiliser dans une classe:

class SpanWrapper 
{ 
    public Span<int> InnerSpan; 
} 

On aura une erreur du type:

"Field or auto-implemented property cannot be of type 'Span<int>' unless it is an instance member. of a ref struct."

Span<T> est un objet de type valeur permettant de traiter des espaces contigus en mémoire sans avoir à effectuer d’allocation. L’intérêt de cet objet est de pouvoir travailler sur des objets managés, non managés ou sur la pile sans forcément être dans un contexte unsafe et d’optimiser les performances en évitant d’effectuer des allocations dans le tas managé.

Concrétement si on doit travailler sur des objets en mémoire, il sera nécessaire d’allouer des tableaux d’objets ou d’effectuer des copies explicites d’objets, par exemple:

byte[] byteArray = new byte[256]; // Allocation 

ou

// Lecture d'un espace en mémoire à partir d'un pointeur: 
IntPtr objectPointer = ...; 
byte[] byteArrayBuffer = new byte[256]; // Allocation 
Marshal.Copy(objectPointer, byteArrayBuffer, 0, byteArrayBuffer.Length); // Copie  

ou

// Ecrire dans un bloc de mémoire non managé: 
SimpleStruct testStruct = new SimpleStruct(); 
IntPtr structPointer = Marshal.AllocHGlobal(Marshal.SizeeOf(textStruct)); // Allocation d'un espace en mémoire non managée 
Marshal.StructureToPtr(testStruct, structPointer, false); // Copie de l'objet dans la mémoire non managée 

Toutes ces opérations ont en commun qu’il est nécessaire d’effectuer des copies pour travailler sur les blocs mémoire ce qui est couteux en performance par rapport à une lecture/écriture directe dans la mémoire. Span<T> vise à éviter ces copies en mettant à disposition une “porte d’entrée” plus directe à des objets en mémoire.

Utilisation de Span<T> suivant le framework

L’objet Span<T> est directement utilisable à partir du .NET Core 2.1 et de C# 7.2 toutefois il n’est pas disponible dans le framework .NET. Pour l’utiliser avec le framework .NET, il faut installer le package NuGet System.Memory.

Il existe 2 implémentations de Span<T> pour lesquelles les performances ne sont pas les mêmes:

  • Pour les frameworksSpan<T> n’est pas disponible nativement comme le framework .NET ou .NET Core 1.1 par exemple: l’implémentation est moins performante (15% moins performante(2)).
  • Pour les frameworksSpan<T> est disponible sans package additionnel : le runtime prends en compte l’objet de façon optimale (6 à 8 fois plus rapide qu’une utilisation sans Span<T>(3)).

Fonctionnement

Span<T> permet de travailler sur des espaces mémoire contenant un nombre fixe d’objets de même type à partir d’une adresse de départ (pas forcément l’adresse du premier objet) et en connaissant la taille des objets. Ainsi les contructeurs de Span<T> autorisent:

  • Un tableau d’objets (new Span<T>(T[] array, int start, int length)): le tableau correspond à l’espace mémoire, start permet de préciser le point de départ à partir duquel on souhaite travailler et length indique le nombre d’objets à traiter. La taille de chaque objet est connue puisque le type des objets est connu.

    Par exemple:

    int[] array = new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; 
    Span<byte> span = new Span<byte>(array, 2, 5); 
    foreach (var item in span) 
      Console.WriteLine(item); // 2, 3, 4, 5, 6 
    
  • Un pointeur et la taille explicite de l’objet (new Span<T>(void* pointer, int length)): le pointeur permet d’indiquer l’adresse de départ de l’espace mémoire, la taille indique le nombre d’objets sur lesquels on veut travailler. La taille des objets est connu puisque le type des objets est connu.

    Par exemple:

    int[] array = new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; 
    unsafe // La manipulation de pointeur impose un contexte unsafe 
    { 
      fixed (int* start = array) // fixed permet d'éviter que l'objet ne soit déplacé par le Garbage Collector. 
      { 
        Span<int> span = new Span<int>(start, 5): 
        foreach (var item in span) 
          Console.WriteLine(item); // 0, 1, 2, 3, 4 
      } 
    } 
    
.AsSpan()

Il est possible de créer une instance de Span<T> en utilisant la méthode d’extension AsSpan():

  • A partir d’un tableau d’objets: au lieu d’écrire
    int[] array = new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; 
    Span<byte> span = new Span<byte>(array); 
    

    On peut écrire directement:

    int[] array = new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; 
    ReaedOnlySpan<int> span = array.AsSpan();
    
  • A partir d’une chaine de caractères:
    ReadOnlySpan<char> span = "Example".AsSpan(); 
    

Toutes ces surcharges de constructeur ont en commun d’avoir une adresse correspondant au point de départ de l’espace mémoire. Cette adresse peut correspondre à un objet managé, un objet sur la pile ou un objet natif. Sachant que Span<T> permet de travailler en commençant à un certain point de l’espace mémoire sans forcément être le premier objet de cet espace, l’adresse peut aussi être un objet à l’intérieur de l’espace mémoire:


Ainsi dans le Span<T>, l’adresse est stockée sous la forme d’un interior pointer. Comme on l’a vu précédemment, un interior pointer (objet ByReference) permet de pointer vers un objet de la pile, un objet managé, un objet non managé ou à l’intérieur d’un autre objet:

public readonly ref struct Span<T> 
{ 
  private readonly ref T pointer;

  private readonly int length; 

  // ... 
} 

ref T pointer est l’interior pointer, pour rappel ce type de pointeur n’est pas directement exposé en C# et il doit être stocké exclusivement sur la pile. C’est la raison pour laquelle Span<T> est une ref struct.

stackalloc

stackalloc est un mot-clé qui existe depuis la 1ère version de C#, il permet d’allouer un tableau sur la pile et de retourner un pointeur vers ce tableau. Étant donné que cet opérateur renvoie un pointeur, il ne pouvait être exécuté que dans un contexte unsafe. Par exemple:

unsafe 
{ 
  int* buffer = stackalloc int[256]; // Allocation d’un tableau de 256 entiers sur la pile 
} 

L’intérêt principal d’allouer un tableau sur la pile est de permettre un gain en performance par rapport à une allocation plus classique dans le tas managé, l’allocation sur la pile ne sollicitant pas le garbage collector:

int[] arrayInHeap = new int[256]; 

Le but d’utiliser un pointeur vers un tableau est de pouvoir effectuer des appels dans du code natif en utilisant un pointeur vers un objet de type tableau, par exemple en effectuant un appel Platform Invoke ou un appel dans du code C++/CLI.

C# 7.2

Une innovation a été apportée à stackalloc à partir de C# 7.2: le mot-clé peut, désormais, renvoyer un objet Span<T> ou ReadOnlySpan<T>. Comme on l’a vu plus haut, Span<T> permet d’avoir un point d’entrée performant vers un tableau sans effectuer d’allocations supplémentaires et surtout sans nécessiter d’utiliser un pointeur. Ainsi comme il n’y a pas de pointeurs, il n’est pas forcément nécessaire d’être dans un contexte unsafe:

// unsafe n’est pas nécessaire 

Span<int> buffer = stackalloc int[256]; // les objets sont alloués seulement sur la pile 

La syntaxe est identique pour ReadOnlySpan<T>:

ReadOnlySpan<int> buffer = stackalloc int[256];  

Span<T> est une ref struct qui est un objet de type valeur strictement alloué sur la pile. Il permet donc de tirer partie de l’allocation sur la pile avec stackalloc sans manipuler des pointeurs.

Pour comprendre l’intérêt de l’utilisation de stackalloc et Span<T>, il faut comparer la ligne précédente avec l’exemple:

Span<int> buffer = new int[256]; // les objets sont alloués dans le tas 

Cet exemple est équivalent fonctionnellement à Span<int> buffer = new int[int], ils permettent tous les deux d’accéder aux objets d’un tableau. De plus Span<T> est toujours stocké sur la pile. La différence est qu’avec stackalloc, le tableau est alloué sur la pile alors qu’avec l’opérateur new, le tableau est alloué dans le tas.

Comme on l’a vu précédemment, les surcharges du constructeur de Span<T> autorisent l’utilisation de pointeurs, on peut donc décomposer Span<int> buffer = stackalloc int[256] en plusieurs lignes, la différence étant qu’il faut être dans un contexte unsafe à cause du pointeur:

unsafe 
{ 
  int* buffer = stackalloc int[256]; 
  Span<int> wrapperBuffer = new Span<int>(buffer, 256); 
} 

De même, on peut décomposer la ligne Span<int> buffer = new int[256] en plusieurs lignes en utilisant un pointeur. Il faut en plus, utiliser le mot-clé fixed. En effet, le tableau étant alloué dans le tas managé, fixed va permettre de rendre le tableau fixe en mémoire de façon à empêcher son déplacement par le garbage collector:

int[] buffer = new int[256]; 
unsafe 
{ 
  fixed (int* ptr = buffer) 
  { 
    Span<int> wrapperBuffer = new Span<int>(ptr, buffer.Length); 
  } 
} 

Quantité de mémoire allouée sur la pile

La taille de la pile est limitée en mémoire à:

  • 1 MO pour les processus 32 bits et
  • 4 MO pour les processus 64 bits.

Contrairement au tas managé, il faut prendre des précautions pour limiter les allocations sur la pile en utilisant stackalloc et éviter une exception de type StackOverflowException si la taille de la pile est dépassée.

Par exemple:
Il est préférable de limiter la taille du buffer à une constante ou une valeur bornée:

Span<int> buffer = stackallow int[256]; // OK 

void MakeProcessWithBuffer(int bufferSize) 
{ 
  Span<int> buffer = stackalloc int[bufferSize]; // A éviter 
  // ... 
} 

Dans le cas précédent, la valeur de bufferSize peut être grande, il faut regarder le corps de la méthode pour comprendre que cette valeur est utilisé par stackalloc. On peut utiliser des garde-fous pour borner la valeur de bufferSize, par exemple:

bool TryMakeProcessWithBufferOnStack(int bufferSize) 
{ 
  if (bufferSize > 1024) return false; 

  Span<int> buffer = stackalloc int[bufferSize];  
  // ... 
} 

On alloue le buffer dans le tas managé dans le cas où bufferSize dépasse une certaine valeur:

bool MakeProcessWithBuffer(int bufferSize) 
{ 
  Span<int> buffer = bufferSize > 1024 ? new int[bufferSize] : stackalloc int[bufferSize];  
  // ... 
} 

De la même façon, il est préférable d’éviter d’utiliser stackalloc à l’intérieur d’une boucle, on peut effectuer l’allocation à l’extérieur de la boucle et utiliser le même objet pour toutes les boucles.

Par exemple:

void MakeProcessInLoop(int loopNumber) 
{ 
  for (int i = 0; i < loopNumber; i++) 
  { 
    Span<int> buffer = stackalloc int[256];  
    // ... 
  } 
} 

Les performances de la méthode sont liées au nombre de boucle. En allouant le tableau à l’extérieur de la boucle, la méthode sera moins tributaire de ce nombre de boucle, quitte à réinitialiser la tableau:

void MakeProcessInLoop(int loopNumber) 
{ 
  Span<int> buffer = stackalloc int[256];  
  for (int i = 0; i < loopNumber; i++) 
  { 
    buffer.Clear(); // Permet de réinitialiser les valeurs du tableau avec une valeur par défaut 

    buffer.Fill(0); // Permet d'affecter une même valeur à tous les éléments du tableau    
    // ... 
  } 
} 

Utilisation de stackalloc dans une expression

C# 8.0

Quand stackalloc est apparu en C# 7.2, il ne permettait d’être utilisé que pour initialiser une variable de type Span<T> ou ReadOnlySpan<T>:

Span<int> buffer = stackalloc int[256];
ReadOnlySpan<int> readOnlyBuffer = stackalloc int[256];

A partir de C# 8.0, il est possible d’utiliser stackalloc dans une expression en dehors d’une affectation.
Ainsi, Span<T> et ReadOnlySpan<T> étant énumérables, on peut, par exemple, utiliser le résultat de stackalloc directement dans une boucle foreach:

foreach (var number in stackalloc[] {1, 2, 3, 4, 5})
{
  // ...
}

De même on peut utiliser directement le résultat de stackalloc dans une expression utilisant les méthodes d’extension dans System.MemoryExtensions sans passer par une variable intermédiaire, par exemple:

Span<int> numbers = stackalloc[] { 1, 2, 3, 4, 5 };
bool comparisonResult = numbers.SequenceEqual(stackalloc[] { 1, 2, 3, 4, 5, 6 });

readonly ref struct

C# 7.2

On peut utiliser à la fois readonly et ref devant struct pour cumuler les caractéristiques de ref struct et readonly struct:

  • ref struct pour contraindre une structure à être stockée exclusivement sur la pile et
  • readonly struct pour rendre une structure immutable.

Quand on déclare la structure, le mot-clé readonly doit se trouver obligatoirement avant ref:

readonly ref struct ImmutableStackOnlyStruct  
{ ... }  

Ainsi une structure de ce type comporte de nombreuses restrictions correspondant aux restrictions pour ref struct et readonly struct:

  • Ne peut pas être stockée dans un tableau (car elle doit être stockée exclusivement sur la pile).
  • Ne peut pas être membre d’une classe (sinon elle serait stockée dans le tas managé).
  • Ne peut pas satisfaire une interface (car elle pourrait entraîner du boxing).
  • Ne doit pas comporter des arguments génériques.
  • Ne peut pas être un argument d’une expression lambda.
  • Ne peut pas être utilisée dans une méthode async.
  • Ne peut pas être utilisée dans un itérateur.
  • Ne doit pas comporter des accesseurs autorisant l’écriture.
  • Ne doit pas comporter des variables membres publiques.
  • Ne doit pas contenir des déclarations d’évènements.

ref struct et readonly ref struct disposable

C# 8.0

A partir de C# 8.0, les structures de type ref struct ou readonly ref struct peuvent être disposable. Sachant que les ref struct et les readonly ref struct sont stockées seulement sur la pile, il n’est pas possible de les faire satisfaire une interface, on ne peut donc pas implémenter IDisposable. A partir de C# 8.0, si une ref struct ou une readonly ref struct implémente une méthode publique void Dispose() alors la structure sera disposable sans avoir à rajouter explicitement : IDisposable.

Par exemple, si on considère la structure suivante:

ref struct DisposableStruct
{
  public Dispose()
  {
    Console.WriteLine("Disposed");
  }
}

En exécutant le code suivant, la méthode Dispose() est bien exécutée:

using (new DisposableStruct())
{
}

Références

Ref struct:

GC:

Managed pointers:

Span:

Stackalloc:

Leave a Reply