196 votes

Utilisation pratique du mot-clé `stackalloc`.

Est-ce que quelqu'un a déjà utilisé stackalloc en programmant en C# ? Je suis conscient de ce qu'il fait, mais la seule fois où il apparaît dans mon code est par accident, parce qu'Intellisense le suggère lorsque je commence à taper. static par exemple.

Bien qu'elle ne soit pas liée aux scénarios d'utilisation de la stackalloc En fait, je fais une quantité considérable d'interopérabilité avec les applications patrimoniales dans mes applications. unsafe code. Mais néanmoins, je trouve généralement des moyens d'éviter unsafe complètement.

Et comme la taille de la pile pour un seul thread dans .Net est de ~1Mb (corrigez-moi si je me trompe), je suis encore plus réservé à l'utilisation de stackalloc .

Y a-t-il des cas pratiques où l'on pourrait dire : "c'est exactement la bonne quantité de données et de traitement pour que je puisse aller dans le danger et utiliser". stackalloc " ?

10 votes

Je viens de remarquer que System.Numbers l'utilise beaucoup referencesource.microsoft.com/#mscorlib/system/

219voto

Pop Catalin Points 25033

La seule raison d'utiliser stackalloc est la performance (que ce soit pour les calculs ou l'interopérabilité). En utilisant stackalloc au lieu d'un tableau alloué au tas, vous créez moins de pression sur la GC (la GC doit moins s'exécuter), vous n'avez pas besoin de bloquer les tableaux, ils sont plus rapides à allouer qu'un tableau au tas, et ils sont automatiquement libérés à la sortie de la méthode (les tableaux alloués au tas ne sont désalloués que lorsque la GC s'exécute). De plus, en utilisant stackalloc au lieu d'un allocateur natif (comme malloc ou l'équivalent .Net), vous gagnez également en rapidité et en désallocation automatique à la sortie de la portée.

En termes de performances, si vous utilisez stackalloc vous augmentez considérablement les chances d'obtenir des résultats dans le cache de l'unité centrale en raison de la proximité des données.

35 votes

Localité des données, bon point ! C'est ce que la mémoire gérée permet rarement de réaliser quand on veut allouer plusieurs structures ou tableaux. Merci !

26 votes

Les allocations de tas sont généralement plus rapides pour les objets gérés que pour les objets non gérés car il n'y a pas de liste libre à parcourir ; le CLR se contente d'incrémenter le pointeur de tas. En ce qui concerne la localité, les allocations séquentielles sont plus susceptibles de se retrouver en colocalisation pour les processus gérés à long terme en raison de la compaction du tas.

2 votes

"il est plus rapide à allouer qu'un tableau de tas" Pourquoi ça ? Juste la localité ? Dans tous les cas, c'est juste une bosse de pointeur, non ?

49voto

Jim Arnold Points 1430

J'ai utilisé stackalloc pour allouer des tampons pour des travaux de DSP en temps [quasi] réel. C'était un cas très spécifique où les performances devaient être aussi cohérentes que possible. Notez qu'il y a une différence entre la cohérence et le débit global - dans ce cas, je n'étais pas préoccupé par le fait que les allocations de tas soient trop lentes, mais seulement par le caractère non déterministe du ramassage des déchets à ce moment du programme. Je ne l'utiliserais pas dans 99% des cas.

45voto

anth Points 1187

Initialisation des spans par Stackalloc. Dans les versions précédentes de C#, le résultat de stackalloc ne pouvait être stocké que dans une variable locale pointeur. À partir de C# 7.2, stackalloc peut maintenant être utilisé comme partie d'une expression et peut cibler un span, et ce sans utiliser le mot-clé unsafe. Ainsi, au lieu d'écrire

Span<byte> bytes;
unsafe
{
  byte* tmp = stackalloc byte[length];
  bytes = new Span<byte>(tmp, length);
}

Vous pouvez écrire simplement :

Span<byte> bytes = stackalloc byte[length];

C'est également extrêmement utile dans les situations où vous avez besoin d'un peu d'espace scratch pour effectuer une opération, mais où vous voulez éviter d'allouer de la mémoire de tas pour des tailles relativement petites.

Span<byte> bytes = length <= 128 ? stackalloc byte[length] : new byte[length];
... // Code that operates on the Span<byte>

Source : C# - Tout sur le Span : Exploration d'un nouveau pilier de .NET

26voto

Brian Rasmussen Points 68853

stackalloc n'est pertinent que pour le code non sécurisé. Pour le code géré, vous ne pouvez pas décider où allouer les données. Les types de valeurs sont alloués sur la pile par défaut (sauf s'ils font partie d'un type de référence, auquel cas ils sont alloués sur le tas). Les types de référence sont alloués sur le tas.

La taille de la pile par défaut pour une application .NET classique est de 1 Mo, mais vous pouvez la modifier dans l'en-tête du PE. Si vous démarrez des threads de manière explicite, vous pouvez également définir une taille différente via la surcharge du constructeur. Pour les applications ASP.NET, la taille de la pile par défaut n'est que de 256 Ko, ce qui est à prendre en compte si vous passez d'un environnement à l'autre.

0 votes

Est-il possible de modifier la taille de la pile par défaut à partir de Visual Studio ?

0 votes

@configurator : Pas à ma connaissance.

9 votes

Ce n'est plus le cas avec la nouvelle version de C# 7.2. Span<T> y ReadOnlySpan<T> . @Anth l'a signalé dans une réponse ci-dessous.

12voto

Tono Nam Points 4465

Réponse tardive mais je pense qu'elle est utile.

Je suis arrivé à cette question et j'étais toujours curieux de voir la différence de performance. J'ai donc créé le benchmark suivant (en utilisant le paquet NuGet BenchmarkDotNet) :

[MemoryDiagnoser]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[RankColumn]
public class Benchmark1
{
    //private MemoryStream ms = new MemoryStream();

    static void FakeRead(byte[] buffer, int start, int length)
    {
        for (int i = start; i < length; i++)
            buffer[i] = (byte) (i % 250);
    }

    static void FakeRead(Span<byte> buffer)
    {
        for (int i = 0; i < buffer.Length; i++)
            buffer[i] = (byte) (i % 250);
    }

    [Benchmark]
    public void AllocatingOnHeap()
    {
        var buffer = new byte[1024];
        FakeRead(buffer, 0, buffer.Length);
    }

    [Benchmark]
    public void ConvertingToSpan()
    {
        var buffer = new Span<byte>(new byte[1024]);
        FakeRead(buffer);
    }

    [Benchmark]
    public void UsingStackAlloc()
    {
        Span<byte> buffer = stackalloc byte[1024];
        FakeRead(buffer);
    }
}

Et voici les résultats

|           Method |     Mean |    Error |   StdDev | Rank |  Gen 0 | Allocated |
|----------------- |---------:|---------:|---------:|-----:|-------:|----------:|
|  UsingStackAlloc | 704.9 ns | 13.81 ns | 12.91 ns |    1 |      - |         - |
| ConvertingToSpan | 755.8 ns |  5.77 ns |  5.40 ns |    2 | 0.0124 |   1,048 B |
| AllocatingOnHeap | 839.3 ns |  4.52 ns |  4.23 ns |    3 | 0.0124 |   1,048 B |

Ce benchmark montre que l'utilisation de stackalloc est la solution la plus rapide et elle n'utilise aucune allocation ! Si vous êtes curieux de savoir comment utiliser le paquet NuGet BenchmarkDotNet, regardez cette vidéo .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X