web-dev-qa-db-fra.com

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

Je travaille actuellement sur un programme très critique pour les performances 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[]s) que j'accéderai à la pile (en utilisant stackalloc ).

J'ai lire que la taille de pile par défaut pour un thread est de 1 Mo, donc pour déplacer tous mes float[]s Je devrais étendre 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 un 530% augmentation 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;
    }
}
228
Sam

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'allocation , cependant, est la plus rapide sur la pile et la plus lente 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 font jamais - laisser 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 est conservé uniquement à 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 la mémoire allouée au tas (prend 120% du temps) pour une utilisation dans les tableaux!

Ceci est mon code de test , et ceci est 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.

Modifier : 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 soit en fait ralentit.

45
Vercas

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 n'accomplit 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 également en C #, vous devez utiliser le mot clé stackalloc. Si vous faites cela, alors le danger évident est d'avoir à écrire du code non sécurisé qui est sujet à de telles attaques, ainsi qu'une 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 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.

28
Hans Passant

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);
}
22
Marc Gravell

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.

8
PMF

Microbenchmarking langages avec JIT et GC tels que Java ou C # peut être un peu compliqué, donc c'est généralement une bonne idée d'utiliser un framework existant - Java propose mhf ou Caliper qui sont excellents, au meilleur de ma connaissance, C # n'offre rien qui s'approche d'eux. Jon Skeet a écrit this 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 faire).

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 fonctionnement du GC, 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);
}
6
Voo

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, il n'y aura aucun objet 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.

6
MHOOS

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 jettent spécification ECMA :

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

Exceptions qu'il jette:

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, stelem fonctionne davantage dans la vérification des plages de tableaux et la vérification des types. É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).

5
HKTonyLee

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 l'OP TestMethod2 cela 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 s'exécutent à des moments très similaires. M5 est la plus rapide (et M1 est proche en deuxième position). 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);
        }
    }
4
Dialecticus