Quels sont les dangers lors de la création d'un thread avec une taille de pile de 50x la valeur par défaut?


228

Je travaille actuellement sur un programme très performant et un chemin que j'ai décidé d'explorer qui pourrait aider à réduire la consommation de ressources augmentait la taille de la pile de mes threads de travail afin que je puisse déplacer la plupart des données float[]auxquelles j'accéderais. la pile (en utilisant stackalloc).

J'ai lu que la taille de pile par défaut pour un thread est de 1 Mo, donc pour déplacer tous mes float[]fichiers, je devrais agrandir la pile d'environ 50 fois (jusqu'à 50 Mo ~).

Je comprends que cela est généralement considéré comme «dangereux» et n'est pas recommandé, mais après avoir comparé mon code actuel à cette méthode, j'ai découvert une augmentation de 530% de la vitesse de traitement! Je ne peux donc pas simplement passer par cette option sans une enquête plus approfondie, ce qui m'amène à ma question; quels sont les dangers associés à l'augmentation de la pile à une taille aussi grande (ce qui pourrait mal tourner), et quelles précautions dois-je prendre pour minimiser ces dangers?

Mon code de test,

public static unsafe void TestMethod1()
{
    float* samples = stackalloc float[12500000];

    for (var ii = 0; ii < 12500000; ii++)
    {
        samples[ii] = 32768;
    }
}

public static void TestMethod2()
{
    var samples = new float[12500000];

    for (var i = 0; i < 12500000; i++)
    {
        samples[i] = 32768;
    }
}

98
+1. Sérieusement. Vous demandez ce qui semble être une question idiote hors de la norme, puis vous montrez TRÈS bien que, dans votre scénario particulier, il est judicieux de le considérer car vous avez fait vos devoirs et mesuré le résultat. C'est TRÈS bon - ça me manque avec beaucoup de questions. Très bien - bon vous envisagez quelque chose comme ça, malheureusement beaucoup de programmeurs C # ne sont pas conscients de ces opportunités d'optimisation. Oui, souvent pas nécessaire - mais parfois c'est critique et fait une énorme différence.
TomTom

5
Je suis intéressé de voir les deux codes qui ont une différence de 530% dans la vitesse de traitement, uniquement en raison du déplacement du tableau vers la pile. Cela ne me semble pas juste.
Dialecticus

13
Avant de vous lancer dans cette voie: avez-vous essayé d'utiliser Marshal.AllocHGlobal(n'oubliez pas FreeHGlobaltrop) pour allouer les données en dehors de la mémoire gérée? Ensuite, placez le pointeur sur a float*, et vous devriez être trié.
Marc Gravell

2
Cela semble bien si vous faites beaucoup d'allocations. Stackalloc contourne tous les problèmes de GC qui peuvent également créer / créent une localité très forte au niveau du processeur. C'est l'une des choses qui ressemblent à des micro-optimisations - à moins que vous n'écriviez un programme mathématique haute performance et que vous ayez exactement ce comportement et que cela fasse une différence;)
TomTom

6
Mon soupçon: l'une de ces méthodes déclenche la vérification des limites à chaque itération de boucle tandis que l'autre ne le fait pas, ou elle est optimisée.
pjc50

Réponses:


45

En comparant le code de test avec Sam, j'ai déterminé que nous avions tous les deux raison!
Cependant, à propos de différentes choses:

  • L'accès à la mémoire (lecture et écriture) est tout aussi rapide où qu'il soit - pile, global ou tas.
  • L'allouer , cependant, est le plus rapide sur la pile et le plus lent sur le tas.

Il va comme ceci: stack< global< heap. (temps d'allocation)
Techniquement, l'allocation de pile n'est pas vraiment une allocation, le runtime s'assure juste qu'une partie de la pile (trame?) est réservée au tableau.

Cependant, je vous conseille vivement de faire attention à cela.
Je recommande ce qui suit:

  1. Lorsque vous devez créer fréquemment des tableaux qui ne quittent jamais la fonction (par exemple en passant sa référence), l'utilisation de la pile sera une énorme amélioration.
  2. Si vous pouvez recycler un tableau, faites-le chaque fois que vous le pouvez! Le tas est le meilleur endroit pour le stockage d'objets à long terme. (la mémoire globale polluante n'est pas agréable; les trames de pile peuvent disparaître)

( Remarque : 1. s'applique uniquement aux types de valeur; les types de référence seront alloués sur le tas et l'avantage sera réduit à 0)

Pour répondre à la question elle-même: je n'ai rencontré aucun problème avec un test à grande pile.
Je crois que les seuls problèmes possibles sont un débordement de pile, si vous ne faites pas attention à vos appels de fonction et si vous manquez de mémoire lors de la création de vos threads si le système est faible.

La section ci-dessous est ma réponse initiale. C'est faux et les tests ne sont pas corrects. Il n'est conservé qu'à titre de référence.


Mon test indique que la mémoire allouée à la pile et la mémoire globale sont au moins 15% plus lentes que (prend 120% du temps de) la mémoire allouée au tas pour une utilisation dans les tableaux!

Voici mon code de test , et voici un exemple de sortie:

Stack-allocated array time: 00:00:00.2224429
Globally-allocated array time: 00:00:00.2206767
Heap-allocated array time: 00:00:00.1842670
------------------------------------------
Fastest: Heap.

  |    S    |    G    |    H    |
--+---------+---------+---------+
S |    -    | 100.80 %| 120.72 %|
--+---------+---------+---------+
G |  99.21 %|    -    | 119.76 %|
--+---------+---------+---------+
H |  82.84 %|  83.50 %|    -    |
--+---------+---------+---------+
Rates are calculated by dividing the row's value to the column's.

J'ai testé sur Windows 8.1 Pro (avec Update 1), en utilisant un i7 4700 MQ, sous .NET 4.5.1
J'ai testé à la fois avec x86 et x64 et les résultats sont identiques.

Edit : j'ai augmenté la taille de la pile de tous les threads 201 Mo, la taille de l'échantillon à 50 millions et diminué les itérations à 5.
Les résultats sont les mêmes que ci - dessus :

Stack-allocated array time: 00:00:00.4504903
Globally-allocated array time: 00:00:00.4020328
Heap-allocated array time: 00:00:00.3439016
------------------------------------------
Fastest: Heap.

  |    S    |    G    |    H    |
--+---------+---------+---------+
S |    -    | 112.05 %| 130.99 %|
--+---------+---------+---------+
G |  89.24 %|    -    | 116.90 %|
--+---------+---------+---------+
H |  76.34 %|  85.54 %|    -    |
--+---------+---------+---------+
Rates are calculated by dividing the row's value to the column's.

Cependant, il semble que la pile devienne plus lente .


Je dois être en désaccord, selon les résultats de mon benchmark (voir les commentaires en bas de page pour les résultats) montrent que la pile est légèrement plus rapide que globale, et beaucoup plus rapide que le tas; et pour être sûr que mes résultats sont exacts, j'ai effectué le test 20 fois, et chaque méthode a été appelée 100 fois par itération de test. Exécutez-vous définitivement votre benchmark correctement?
Sam

J'obtiens des résultats très incohérents. Avec une confiance totale, x64, une version de configuration, pas de débogueur, ils sont tous aussi rapides (moins de 1% de différence; fluctuant) tandis que le vôtre est en effet beaucoup plus rapide avec une pile. Je dois tester plus loin! Edit : le vôtre DEVRAIT lever une exception de dépassement de pile. Vous allouez simplement assez pour le tableau. O_o
Vercas

Ouais je sais, c'est proche. Vous devez répéter les repères plusieurs fois, comme je l'ai fait, peut-être essayer de faire une moyenne sur 5 ou plus de pistes.
Sam

1
@Voo La 1ère manche a pris autant de temps que la 100ème manche de n'importe quel test pour moi. D'après mon expérience, cette chose Java JIT ne s'applique pas du tout à .NET. Le seul "échauffement" effectué par .NET consiste à charger des classes et des assemblys lors de leur première utilisation.
Vercas

2
@Voo Testez mon indice de référence et celui de l'essentiel qu'il a ajouté dans un commentaire à cette réponse. Assemblez les codes ensemble et exécutez quelques centaines de tests. Revenez ensuite et faites part de votre conclusion. J'ai fait mes tests très minutieusement, et je sais très bien de quoi je parle lorsque je dis que .NET n'interprète aucun bytecode comme Java, il le JIT instantanément.
Vercas

28

J'ai découvert une augmentation de 530% de la vitesse de traitement!

C'est de loin le plus grand danger que je dirais. Il y a quelque chose de grave avec votre référence, le code qui se comporte de manière imprévisible a généralement un bug méchant caché quelque part.

Il est très, très difficile de consommer beaucoup d'espace de pile dans un programme .NET, autrement que par récursivité excessive. La taille du cadre de pile des méthodes gérées est définie dans la pierre. Simplement la somme des arguments de la méthode et des variables locales dans une méthode. Moins ceux qui peuvent être stockés dans un registre CPU, vous pouvez ignorer cela car il y en a si peu.

Augmenter la taille de la pile ne fait rien, vous réserverez juste un tas d'espace d'adressage qui ne sera jamais utilisé. Il n'y a aucun mécanisme qui puisse expliquer une augmentation de la performance en n'utilisant pas de mémoire bien sûr.

Contrairement à un programme natif, en particulier un programme écrit en C, il peut également réserver de l'espace pour les tableaux sur le cadre de pile. Le vecteur d'attaque de malware de base derrière les débordements de tampon de pile. Possible en C # également, vous devez utiliser le stackallocmot clé. Si vous faites cela, le danger évident est d'écrire du code non sécurisé qui est soumis à de telles attaques, ainsi que la corruption de trame de pile aléatoire. Très difficile à diagnostiquer les bugs. Il existe une contre-mesure contre cela dans les tremblements ultérieurs, je pense à partir de .NET 4.0, où le tremblement génère du code pour placer un "cookie" sur le cadre de la pile et vérifie s'il est toujours intact lorsque la méthode revient. Crash instantané sur le bureau sans aucun moyen d'intercepter ou de signaler l'incident si cela se produit. C'est ... dangereux pour l'état mental de l'utilisateur.

Le thread principal de votre programme, celui démarré par le système d'exploitation, aura une pile de 1 Mo par défaut, 4 Mo lorsque vous compilerez votre programme en ciblant x64. Augmenter cela nécessite l'exécution de Editbin.exe avec l'option / STACK dans un événement de post-génération. Vous pouvez généralement demander jusqu'à 500 Mo avant que votre programme ait du mal à démarrer lors de l'exécution en mode 32 bits. Les threads peuvent aussi, beaucoup plus faciles bien sûr, la zone de danger plane généralement autour de 90 Mo pour un programme 32 bits. Déclenché lorsque votre programme fonctionne depuis longtemps et que l'espace d'adressage s'est fragmenté par rapport aux allocations précédentes. L'utilisation totale de l'espace d'adressage doit déjà être élevée, sur un concert, pour obtenir ce mode de défaillance.

Vérifiez votre code, il y a quelque chose de très mal. Vous ne pouvez pas obtenir une accélération x5 avec une plus grande pile à moins que vous n'écriviez explicitement votre code pour en profiter. Ce qui nécessite toujours un code dangereux. L'utilisation de pointeurs en C # a toujours un talent pour créer un code plus rapide, il n'est pas soumis aux vérifications des limites du tableau.


21
L'accélération 5x signalée était de passer de float[]à float*. La grande pile était simplement la façon dont cela a été accompli. Une accélération x5 dans certains scénarios est tout à fait raisonnable pour ce changement.
Marc Gravell

3
D'accord, je n'avais pas encore l'extrait de code lorsque j'ai commencé à répondre à la question. Toujours assez proche.
Hans Passant

22

J'aurais une réserve là-bas que je ne saurais tout simplement pas prédire - les autorisations, le GC (qui doit analyser la pile), etc. - tout pourrait être affecté. Je serais très tenté d'utiliser à la place de la mémoire non managée:

var ptr = Marshal.AllocHGlobal(sizeBytes);
try
{
    float* x = (float*)ptr;
    DoWork(x);
}
finally
{
    Marshal.FreeHGlobal(ptr);
}

1
Question secondaire: Pourquoi le GC aurait-il besoin de scanner la pile? La mémoire allouée par stackallocn'est pas soumise à la récupération de place.
dcastro

6
@dcastro, il doit analyser la pile pour vérifier les références qui n'existent que sur la pile. Je ne sais tout simplement pas ce qu'il va faire quand il arrivera à un si grand stackalloc- il doit en quelque sorte le sauter, et vous espérez qu'il le fasse sans effort - mais le point que j'essaie de faire est qu'il introduit complications / préoccupations inutiles . IMO, stackallocest génial comme tampon de travail, mais pour un espace de travail dédié, il est plus censé simplement allouer un morceau de mémoire quelque part, plutôt que d'abuser / de confondre la pile,
Marc Gravell

8

Une chose qui peut mal tourner est que vous pourriez ne pas obtenir la permission de le faire. À moins qu'il ne s'exécute en mode de confiance totale, le Framework ignorera simplement la demande d'une taille de pile plus grande (voir MSDN sur Thread Constructor (ParameterizedThreadStart, Int32))

Au lieu d'augmenter la taille de la pile système à un nombre aussi élevé, je suggère de réécrire votre code afin qu'il utilise l'itération et une implémentation manuelle de la pile sur le tas.


1
Bonne idée, je vais parcourir à la place. En plus de cela, mon code fonctionne en mode de confiance totale, alors y a-t-il d'autres choses à surveiller?
Sam

6

Les tableaux hautement performants peuvent être accessibles de la même manière qu'un C # normal, mais cela pourrait être le début d'un problème: considérez le code suivant:

float[] someArray = new float[100]
someArray[200] = 10.0;

Vous vous attendez à une exception hors limite et cela a tout à fait du sens parce que vous essayez d'accéder à l'élément 200 mais la valeur maximale autorisée est 99. Si vous allez sur la route stackalloc, aucun objet ne sera enroulé autour de votre tableau à vérifier et le ce qui suit ne montrera aucune exception:

Float* pFloat =  stackalloc float[100];
fFloat[200]= 10.0;

Ci-dessus, vous allouez suffisamment de mémoire pour contenir 100 flottants et vous définissez l'emplacement de la mémoire sizeof (float) qui commence à l'emplacement commencé de cette mémoire + 200 * sizeof (float) pour contenir votre valeur flottante 10. Sans surprise, cette mémoire est en dehors de la alloué de la mémoire pour les flotteurs et personne ne saurait ce qui pourrait être stocké dans cette adresse. Si vous êtes chanceux, vous avez peut-être utilisé de la mémoire actuellement inutilisée, mais en même temps, il est probable que vous puissiez remplacer un emplacement qui a été utilisé pour stocker d'autres variables. Pour résumer: Comportement d'exécution imprévisible.


En fait, c'est faux. Les tests d'exécution et de compilation sont toujours là.
TomTom

9
@TomTom erm, non; la réponse a du mérite; la question parle de stackalloc, dans ce cas, nous parlons de float*etc - qui n'a pas les mêmes contrôles. Il est appelé unsafepour une très bonne raison. Personnellement, je suis parfaitement heureux de l'utiliser unsafequand il y a une bonne raison, mais Socrates fait quelques remarques raisonnables.
Marc Gravell

@Marc Pour le code affiché (après l'exécution du JIT), il n'y a plus de vérification des limites car il est trivial pour le compilateur de penser que tous les accès sont entrants. En général, cela peut certainement faire une différence.
Voo

6

Les langages de micro-analyse avec JIT et GC tels que Java ou C # peuvent être un peu compliqués, donc c'est généralement une bonne idée d'utiliser un framework existant - Java propose mhf ou Caliper qui sont excellents, malheureusement au meilleur de ma connaissance, C # n'offre pas tout ce qui se rapproche de ceux-ci. Jon Skeet a écrit ceci ici que je suppose aveuglément prendre en charge les choses les plus importantes (Jon sait ce qu'il fait dans ce domaine; aussi oui, pas de soucis, j'ai vérifié). J'ai légèrement modifié le timing car 30 secondes par test après l'échauffement étaient trop pour ma patience (5 secondes devraient suffire).

Donc, d'abord les résultats, .NET 4.5.1 sous Windows 7 x64 - les chiffres indiquent les itérations qu'il pourrait exécuter en 5 secondes, donc plus c'est mieux.

x64 JIT:

Standard       10,589.00  (1.00)
UnsafeStandard 10,612.00  (1.00)
Stackalloc     12,088.00  (1.14)
FixedStandard  10,715.00  (1.01)
GlobalAlloc    12,547.00  (1.18)

x86 JIT (ouais c'est encore un peu triste):

Standard       14,787.00   (1.02)
UnsafeStandard 14,549.00   (1.00)
Stackalloc     15,830.00   (1.09)
FixedStandard  14,824.00   (1.02)
GlobalAlloc    18,744.00   (1.29)

Cela donne une accélération beaucoup plus raisonnable d'au plus 14% (et la majeure partie des frais généraux est due au fait que le GC doit fonctionner, considérez-le comme le pire des cas de manière réaliste). Les résultats x86 sont cependant intéressants - pas tout à fait clair ce qui se passe là-bas.

et voici le code:

public static float Standard(int size) {
    float[] samples = new float[size];
    for (var ii = 0; ii < size; ii++) {
        samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
    }
    return samples[size - 1];
}

public static unsafe float UnsafeStandard(int size) {
    float[] samples = new float[size];
    for (var ii = 0; ii < size; ii++) {
        samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
    }
    return samples[size - 1];
}

public static unsafe float Stackalloc(int size) {
    float* samples = stackalloc float[size];
    for (var ii = 0; ii < size; ii++) {
        samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
    }
    return samples[size - 1];
}

public static unsafe float FixedStandard(int size) {
    float[] prev = new float[size];
    fixed (float* samples = &prev[0]) {
        for (var ii = 0; ii < size; ii++) {
            samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
        }
        return samples[size - 1];
    }
}

public static unsafe float GlobalAlloc(int size) {
    var ptr = Marshal.AllocHGlobal(size * sizeof(float));
    try {
        float* samples = (float*)ptr;
        for (var ii = 0; ii < size; ii++) {
            samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
        }
        return samples[size - 1];
    } finally {
        Marshal.FreeHGlobal(ptr);
    }
}

static void Main(string[] args) {
    int inputSize = 100000;
    var results = TestSuite.Create("Tests", inputSize, Standard(inputSize)).
        Add(Standard).
        Add(UnsafeStandard).
        Add(Stackalloc).
        Add(FixedStandard).
        Add(GlobalAlloc).
        RunTests();
    results.Display(ResultColumns.NameAndIterations);
}

Une observation intéressante, je vais devoir vérifier à nouveau mes repères. Bien que cela ne réponde toujours pas vraiment à ma question, " ... quels sont les dangers associés à l'augmentation de la pile à une si grande taille ... ". Même si mes résultats sont incorrects, la question est toujours valable; J'apprécie néanmoins l'effort.
Sam

1
@Sam Lorsque j'utilise 12500000comme taille, j'obtiens en fait une exception stackoverflow. Mais il s'agissait surtout de rejeter la prémisse sous-jacente selon laquelle l'utilisation de code alloué par pile est plus rapide de plusieurs ordres de grandeur. Sinon, nous faisons à peu près le moins de travail possible et la différence n'est déjà que de 10 à 15% - en pratique, elle sera encore plus faible .. cela, à mon avis, change définitivement toute la discussion.
Voo

5

La différence de performances étant trop importante, le problème est à peine lié à l'allocation. Cela est probablement dû à l'accès à la baie.

J'ai démonté le corps de la boucle des fonctions:

TestMethod1:

IL_0011:  ldloc.0 
IL_0012:  ldloc.1 
IL_0013:  ldc.i4.4 
IL_0014:  mul 
IL_0015:  add 
IL_0016:  ldc.r4 32768.
IL_001b:  stind.r4 // <----------- This one
IL_001c:  ldloc.1 
IL_001d:  ldc.i4.1 
IL_001e:  add 
IL_001f:  stloc.1 
IL_0020:  ldloc.1 
IL_0021:  ldc.i4 12500000
IL_0026:  blt IL_0011

TestMethod2:

IL_0012:  ldloc.0 
IL_0013:  ldloc.1 
IL_0014:  ldc.r4 32768.
IL_0019:  stelem.r4 // <----------- This one
IL_001a:  ldloc.1 
IL_001b:  ldc.i4.1 
IL_001c:  add 
IL_001d:  stloc.1 
IL_001e:  ldloc.1 
IL_001f:  ldc.i4 12500000
IL_0024:  blt IL_0012

Nous pouvons vérifier l'utilisation de l'instruction et, plus important encore, l'exception qu'ils lèvent dans la spécification ECMA :

stind.r4: Store value of type float32 into memory at address

Exceptions qu'il génère:

System.NullReferenceException

Et

stelem.r4: Replace array element at index with the float32 value on the stack.

Exception qu'il lève:

System.NullReferenceException
System.IndexOutOfRangeException
System.ArrayTypeMismatchException

Comme vous pouvez le voir, stelemfonctionne plus dans la vérification de plage de tableau et la vérification de type. Étant donné que le corps de la boucle fait peu de chose (attribue uniquement une valeur), la surcharge de la vérification domine le temps de calcul. C'est pourquoi les performances diffèrent de 530%.

Et cela répond également à vos questions: le danger est l'absence de vérification de la gamme et du type de réseau. Ceci n'est pas sûr (comme mentionné dans la déclaration de fonction; D).


4

EDIT: (un petit changement de code et de mesure produit un grand changement dans le résultat)

Tout d'abord, j'ai exécuté le code optimisé dans le débogueur (F5) mais c'était faux. Il doit être exécuté sans le débogueur (Ctrl + F5). Deuxièmement, le code peut être complètement optimisé, nous devons donc le compliquer afin que l'optimiseur ne gâche pas notre mesure. J'ai fait que toutes les méthodes retournent un dernier élément dans le tableau, et le tableau est rempli différemment. Il y a aussi un zéro supplémentaire dans les OP TestMethod2qui le rend toujours dix fois plus lent.

J'ai essayé d'autres méthodes, en plus des deux que vous avez fournies. La méthode 3 a le même code que votre méthode 2, mais la fonction est déclarée unsafe. La méthode 4 utilise l'accès par pointeur à un tableau créé régulièrement. La méthode 5 utilise l'accès par pointeur à la mémoire non gérée, comme décrit par Marc Gravell. Les cinq méthodes fonctionnent à des moments très similaires. M5 est le plus rapide (et M1 est proche deuxième). La différence entre le plus rapide et le plus lent est d'environ 5%, ce qui ne m'importe pas.

    public static unsafe float TestMethod3()
    {
        float[] samples = new float[5000000];

        for (var ii = 0; ii < 5000000; ii++)
        {
            samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
        }

        return samples[5000000 - 1];
    }

    public static unsafe float TestMethod4()
    {
        float[] prev = new float[5000000];
        fixed (float* samples = &prev[0])
        {
            for (var ii = 0; ii < 5000000; ii++)
            {
                samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
            }

            return samples[5000000 - 1];
        }
    }

    public static unsafe float TestMethod5()
    {
        var ptr = Marshal.AllocHGlobal(5000000 * sizeof(float));
        try
        {
            float* samples = (float*)ptr;

            for (var ii = 0; ii < 5000000; ii++)
            {
                samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
            }

            return samples[5000000 - 1];
        }
        finally
        {
            Marshal.FreeHGlobal(ptr);
        }
    }

Donc, M3 est le même que M2 marqué uniquement par "dangereux"? Plutôt méfiant que ce serait plus rapide ... êtes-vous sûr?
Roman Starkov

@romkyns Je viens de lancer un benchmark (M2 vs M3), et étonnamment M3 est en fait 2,14% plus rapide que M2.
Sam

" La conclusion est que l'utilisation de la pile n'est pas nécessaire. " Lors de l'allocation de gros blocs tels que ceux que j'ai donnés dans mon article, je suis d'accord, mais, après avoir simplement terminé quelques tests de référence M1 vs M2 (en utilisant l'idée de PFM pour les deux méthodes), je le ferais certainement doivent être en désaccord, car M1 est désormais 135% plus rapide que M2.
Sam

1
@Sam Mais vous comparez toujours l'accès au pointeur à l'accès à la baie! QUE est primarly ce qui le rend plus rapide. TestMethod4vs TestMethod1est une bien meilleure comparaison pour stackalloc.
Roman Starkov

@romkyns Ah oui bon point, j'ai oublié ça; J'ai relancé les benchmarks , il n'y a maintenant qu'une différence de 8% (M1 étant le plus rapide des deux).
Sam
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.