Ich arbeite derzeit an einem sehr leistungskritischen Programm. Ein Pfad, den ich untersuchen wollte, um den Ressourcenverbrauch zu senken, bestand darin float[]
, die Stapelgröße meiner Arbeitsthreads zu erhöhen, damit ich die meisten Daten verschieben kann, auf die ich zugreifen werde der Stapel (mit stackalloc
).
Ich habe gelesen, dass die Standardstapelgröße für einen Thread 1 MB beträgt. Um also alle meine float[]
s zu verschieben, müsste ich den Stapel um ungefähr das 50-fache (auf 50 MB ~) erweitern.
Ich verstehe, dass dies im Allgemeinen als "unsicher" eingestuft wird und nicht empfohlen wird, aber nachdem ich meinen aktuellen Code mit dieser Methode verglichen habe, habe ich eine Steigerung der Verarbeitungsgeschwindigkeit um 530% festgestellt ! Ich kann diese Option also nicht einfach ohne weitere Untersuchung umgehen, was mich zu meiner Frage führt. Welche Gefahren sind mit der Vergrößerung des Stapels auf eine so große Größe verbunden (was könnte schief gehen), und welche Vorsichtsmaßnahmen sollte ich treffen, um solche Gefahren zu minimieren?
Mein Testcode,
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;
}
}
quelle
Marshal.AllocHGlobal
(nicht zu vergessenFreeHGlobal
), die Daten außerhalb des verwalteten Speichers zuzuweisen ? Setzen Sie dann den Zeiger auf afloat*
, und Sie sollten sortiert sein.Antworten:
Beim Vergleich des Testcodes mit Sam stellte ich fest, dass wir beide Recht haben!
Über verschiedene Dinge:
Es geht so:
stack
<global
<heap
. (Zuweisungszeit)Technisch gesehen ist die Stapelzuweisung keine Zuweisung, die Laufzeit stellt lediglich sicher, dass ein Teil des Stapels (Frame?) für das Array reserviert ist.
Ich rate jedoch dringend, vorsichtig damit umzugehen.
Ich empfehle folgendes:
( Hinweis : 1. Gilt nur für Werttypen. Referenztypen werden auf dem Heap zugewiesen und der Nutzen wird auf 0 reduziert.)
Um die Frage selbst zu beantworten: Ich habe bei keinem Large-Stack-Test ein Problem festgestellt.
Ich glaube, die einzig möglichen Probleme sind ein Stapelüberlauf, wenn Sie bei Ihren Funktionsaufrufen nicht vorsichtig sind und beim Erstellen Ihrer Threads nicht genügend Speicher zur Verfügung stehen, wenn das System zur Neige geht.
Der folgende Abschnitt ist meine erste Antwort. Es ist falsch und die Tests sind nicht korrekt. Es wird nur als Referenz aufbewahrt.
Mein Test zeigt, dass der vom Stapel zugewiesene Speicher und der globale Speicher mindestens 15% langsamer sind als der vom Heap zugewiesene Speicher (dauert 120% der Zeit) für die Verwendung in Arrays!
Dies ist mein Testcode und dies ist eine Beispielausgabe:
Ich habe unter Windows 8.1 Pro (mit Update 1) unter Verwendung eines i7 4700 MQ unter .NET 4.5.1
sowohl mit x86 als auch mit x64 getestet und die Ergebnisse sind identisch.
Bearbeiten : Ich habe die Stapelgröße aller Threads um 201 MB, die Stichprobengröße auf 50 Millionen und die Iterationen auf 5 verringert.
Die Ergebnisse sind die gleichen wie oben :
Es scheint jedoch, dass der Stapel tatsächlich langsamer wird .
quelle
Das ist bei weitem die größte Gefahr, die ich sagen würde. Mit Ihrem Benchmark stimmt etwas nicht. Code, der sich so unvorhersehbar verhält, hat normalerweise irgendwo einen bösen Fehler versteckt.
Es ist sehr, sehr schwierig, viel Stapelspeicher in einem .NET-Programm zu verbrauchen, außer durch übermäßige Rekursion. Die Größe des Stapelrahmens verwalteter Methoden ist in Stein gemeißelt. Einfach die Summe der Argumente der Methode und der lokalen Variablen in einer Methode. Abgesehen von denen, die in einem CPU-Register gespeichert werden können, können Sie dies ignorieren, da es so wenige davon gibt.
Durch Erhöhen der Stapelgröße wird nichts erreicht. Sie reservieren lediglich eine Reihe von Adressräumen, die niemals verwendet werden. Es gibt natürlich keinen Mechanismus, der eine Leistungssteigerung erklären kann, wenn das Gedächtnis nicht verwendet wird.
Dies unterscheidet sich von einem nativen Programm, insbesondere einem in C geschriebenen, und kann auch Platz für Arrays auf dem Stapelrahmen reservieren. Der grundlegende Malware-Angriffsvektor hinter dem Stapelpuffer läuft über. Auch in C # möglich, müssten Sie das
stackalloc
Schlüsselwort verwenden. Wenn Sie dies tun, besteht die offensichtliche Gefahr darin, unsicheren Code zu schreiben, der solchen Angriffen ausgesetzt ist, sowie zufällige Stapelrahmenbeschädigungen. Sehr schwer zu diagnostizierende Fehler. Es gibt eine Gegenmaßnahme dagegen in späteren Jitters, ich denke ab .NET 4.0, wo der Jitter Code generiert, um ein "Cookie" auf den Stack-Frame zu setzen und zu überprüfen, ob es bei der Rückkehr der Methode noch intakt ist. Sofortiger Absturz auf dem Desktop, ohne dass das Missgeschick abgefangen oder gemeldet werden kann, wenn dies passiert. Das ist ... gefährlich für den mentalen Zustand des Benutzers.Der Hauptthread Ihres Programms, der vom Betriebssystem gestartet wurde, hat standardmäßig einen Stapel von 1 MB, 4 MB, wenn Sie Ihr Programm für x64 kompilieren. Um dies zu erhöhen, muss Editbin.exe mit der Option / STACK in einem Post-Build-Ereignis ausgeführt werden. Normalerweise können Sie bis zu 500 MB anfordern, bevor Ihr Programm beim Ausführen im 32-Bit-Modus Probleme beim Starten hat. Threads können auch, natürlich viel einfacher, die Gefahrenzone schwebt normalerweise um 90 MB für ein 32-Bit-Programm. Wird ausgelöst, wenn Ihr Programm längere Zeit ausgeführt wurde und der Adressraum aus früheren Zuordnungen fragmentiert wurde. Die gesamte Adressraumnutzung muss bereits über einen Gig hoch sein, um diesen Fehlermodus zu erhalten.
Überprüfen Sie Ihren Code dreimal, da stimmt etwas nicht. Sie können mit einem größeren Stapel keine x5-Beschleunigung erzielen, wenn Sie Ihren Code nicht explizit schreiben, um ihn zu nutzen. Was immer unsicheren Code erfordert. Die Verwendung von Zeigern in C # hat immer ein Händchen für die Erstellung von schnellerem Code. Sie wird nicht den Array-Begrenzungsprüfungen unterzogen.
quelle
float[]
auffloat*
. Der große Stapel war einfach, wie das erreicht wurde. Eine x5-Beschleunigung in einigen Szenarien ist für diese Änderung durchaus sinnvoll.Ich hätte dort einen Vorbehalt, dass ich einfach nicht wissen würde, wie ich ihn vorhersagen soll - Berechtigungen, GC (der den Stapel scannen muss) usw. - alles könnte betroffen sein. Ich wäre sehr versucht, stattdessen nicht verwalteten Speicher zu verwenden:
quelle
stackalloc
unterliegt keiner Speicherbereinigung .stackalloc
- es muss irgendwie springen, und Sie würden hoffen, dass es dies mühelos tun würde -, aber der Punkt, den ich versuche, ist, dass es einführt unnötige Komplikationen / Bedenken. IMOstackalloc
eignet sich hervorragend als Scratch-Buffer, aber für einen dedizierten Arbeitsbereich wird eher erwartet, dass nur ein Chunk-o-Memory irgendwo zugewiesen wird, anstatt den Stapel zu missbrauchen / zu verwirren.Eine Sache, die schief gehen kann, ist, dass Sie möglicherweise nicht die Erlaubnis dazu erhalten. Sofern nicht im Vollvertrauensmodus ausgeführt, ignoriert das Framework nur die Anforderung einer größeren Stapelgröße (siehe MSDN aktiviert
Thread Constructor (ParameterizedThreadStart, Int32)
).Anstatt die Größe des Systemstapels auf so große Zahlen zu erhöhen, würde ich vorschlagen, Ihren Code so umzuschreiben, dass er Iteration und eine manuelle Stapelimplementierung auf dem Heap verwendet.
quelle
Auf die Arrays mit hoher Leistung kann möglicherweise wie auf ein normales C # 1 zugegriffen werden, dies kann jedoch zu Beginn von Problemen führen: Beachten Sie den folgenden Code:
Sie erwarten eine nicht gebundene Ausnahme, und dies ist völlig sinnvoll, da Sie versuchen, auf Element 200 zuzugreifen, der maximal zulässige Wert jedoch 99 beträgt. Wenn Sie zur Stackalloc-Route gehen, wird kein Objekt um Ihr Array gewickelt, um die Prüfung und die gebundene Prüfung durchzuführen Folgendes zeigt keine Ausnahme:
Oben weisen Sie genügend Speicher für 100 Floats zu und legen die Größe des (float) Speicherplatzes fest, der an der Stelle beginnt, an der dieser Speicher gestartet wurde, + 200 * sizeof (float), um Ihren Float-Wert 10 zu halten. Es überrascht nicht, dass sich dieser Speicher außerhalb des Speicherplatzes befindet zugewiesener Speicher für die Floats und niemand würde wissen, was in dieser Adresse gespeichert werden könnte. Wenn Sie Glück haben, haben Sie möglicherweise einen derzeit nicht verwendeten Speicher verwendet, aber gleichzeitig können Sie wahrscheinlich einen Speicherort überschreiben, der zum Speichern anderer Variablen verwendet wurde. Zusammenfassend: Unvorhersehbares Laufzeitverhalten.
quelle
stackalloc
, in welchem Fall wir überfloat*
usw. sprechen - die nicht die gleichen Prüfungen hat. Es hatunsafe
einen sehr guten Grund. Persönlich bin ich sehr glücklich,unsafe
wenn es einen guten Grund gibt, aber Sokrates macht einige vernünftige Punkte.Microbenchmarking-Sprachen mit JIT und GC wie Java oder C # können etwas kompliziert sein, daher ist es im Allgemeinen eine gute Idee, ein vorhandenes Framework zu verwenden - Java bietet mhf oder Caliper an, die ausgezeichnet sind, leider nach meinem besten Wissen, das C # nicht bietet alles, was sich diesen nähert. Jon Skeet hat dies hier geschrieben, von dem ich blindlings annehmen werde, dass er sich um die wichtigsten Dinge kümmert (Jon weiß, was er in diesem Bereich tut; auch ja, keine Sorgen, die ich tatsächlich überprüft habe). Ich habe das Timing ein wenig angepasst, weil 30 Sekunden pro Test nach dem Aufwärmen zu viel für meine Geduld waren (5 Sekunden sollten reichen).
Also zuerst die Ergebnisse, .NET 4.5.1 unter Windows 7 x64 - die Zahlen bezeichnen die Iterationen, die in 5 Sekunden ausgeführt werden könnten, also ist höher besser.
x64 JIT:
x86 JIT (ja, das ist immer noch traurig):
Dies ergibt eine viel vernünftigere Beschleunigung von höchstens 14% (und der größte Teil des Overheads ist darauf zurückzuführen, dass der GC ausgeführt werden muss. Betrachten Sie ihn realistisch als Worst-Case-Szenario). Die x86-Ergebnisse sind jedoch interessant - nicht ganz klar, was dort vor sich geht.
und hier ist der Code:
quelle
12500000
als Größe erhalte ich tatsächlich eine Stackoverflow-Ausnahme. Meist ging es jedoch darum, die zugrunde liegende Prämisse abzulehnen, dass die Verwendung von Stapel-zugewiesenem Code mehrere Größenordnungen schneller ist. Wir machen hier so ziemlich den geringstmöglichen Arbeitsaufwand und der Unterschied beträgt bereits nur etwa 10-15% - in der Praxis wird er sogar noch geringer sein. Dies ändert meiner Meinung nach definitiv die gesamte Diskussion.Da der Leistungsunterschied zu groß ist, hängt das Problem kaum mit der Zuordnung zusammen. Dies wird wahrscheinlich durch den Array-Zugriff verursacht.
Ich habe den Schleifenkörper der Funktionen zerlegt:
TestMethod1:
TestMethod2:
Wir können die Verwendung der Anweisung und vor allem die Ausnahme überprüfen, die sie in der ECMA-Spezifikation auslösen :
Ausnahmen wirft es:
Und
Ausnahme, die es wirft:
Wie Sie sehen können,
stelem
funktioniert mehr bei der Überprüfung des Array-Bereichs und der Typprüfung. Da der Schleifenkörper wenig tut (nur Wert zuweisen), dominiert der Overhead der Prüfung die Rechenzeit. Deshalb unterscheidet sich die Leistung um 530%.Und dies beantwortet auch Ihre Fragen: Die Gefahr besteht darin, dass keine Überprüfung des Array-Bereichs und des Typs erfolgt. Dies ist unsicher (wie in der Funktionsdeklaration erwähnt; D).
quelle
BEARBEITEN: (Eine kleine Änderung des Codes und der Messung führt zu einer großen Änderung des Ergebnisses.)
Zuerst habe ich den optimierten Code im Debugger (F5) ausgeführt, aber das war falsch. Es sollte ohne den Debugger ausgeführt werden (Strg + F5). Zweitens kann der Code gründlich optimiert werden, sodass wir ihn komplizieren müssen, damit der Optimierer unsere Messung nicht beeinträchtigt. Ich habe dafür gesorgt, dass alle Methoden ein letztes Element im Array zurückgeben, und das Array ist anders gefüllt. Außerdem gibt es in OPs eine zusätzliche Null
TestMethod2
, die es immer zehnmal langsamer macht.Ich habe zusätzlich zu den beiden von Ihnen bereitgestellten Methoden einige andere Methoden ausprobiert. Methode 3 hat den gleichen Code wie Ihre Methode 2, aber die Funktion ist deklariert
unsafe
. Methode 4 verwendet den Zeigerzugriff auf regelmäßig erstellte Arrays. Methode 5 verwendet den Zeigerzugriff auf nicht verwalteten Speicher, wie von Marc Gravell beschrieben. Alle fünf Methoden laufen zu sehr ähnlichen Zeiten. M5 ist der schnellste (und M1 ist knapp an zweiter Stelle). Der Unterschied zwischen dem schnellsten und dem langsamsten beträgt etwa 5%, was mir egal ist.quelle
TestMethod4
vsTestMethod1
ist ein viel besserer Vergleich fürstackalloc
.