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;
}
}
Marshal.AllocHGlobal
(n'oubliez pasFreeHGlobal
trop) pour allouer les données en dehors de la mémoire gérée? Ensuite, placez le pointeur sur afloat*
, et vous devriez être trié.Réponses:
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:
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:
( 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:
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 :
Cependant, il semble que la pile devienne plus lente .
la source
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
stackalloc
mot 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.
la source
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.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:
la source
stackalloc
n'est pas soumise à la récupération de place.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,stackalloc
est 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,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.
la source
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:
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:
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.
la source
stackalloc
, dans ce cas, nous parlons defloat*
etc - qui n'a pas les mêmes contrôles. Il est appeléunsafe
pour une très bonne raison. Personnellement, je suis parfaitement heureux de l'utiliserunsafe
quand il y a une bonne raison, mais Socrates fait quelques remarques raisonnables.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:
x86 JIT (ouais c'est encore un peu triste):
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:
la source
12500000
comme 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.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:
TestMethod2:
Nous pouvons vérifier l'utilisation de l'instruction et, plus important encore, l'exception qu'ils lèvent dans la spécification ECMA :
Exceptions qu'il génère:
Et
Exception qu'il lève:
Comme vous pouvez le voir,
stelem
fonctionne 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).
la source
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
TestMethod2
qui 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.la source
TestMethod4
vsTestMethod1
est une bien meilleure comparaison pourstackalloc
.