web-dev-qa-db-de.com

Was sind die Gefahren beim Erstellen eines Threads mit einer Stapelgröße von 50x der Standardeinstellung?

Ich arbeite derzeit an einem sehr leistungskritischen Programm, und ein Weg, den ich zur Reduzierung des Ressourcenverbrauchs beschloss, bestand darin, die Stapelgröße meiner Arbeitsthreads zu erhöhen, damit ich die meisten Daten verschieben kann (float[]s) dass ich auf den Stack zugreifen werde (mit stackalloc ).

Ich habe read , dass die Standardstapelgröße für einen Thread 1 MB beträgt, um also alle meine float[]s Ich müsste den Stack ungefähr 50-mal erweitern (auf 50 MB ~).

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 festgestellt, dass 530% Erhöhung der Verarbeitungsgeschwindigkeit! Ich kann diese Option also nicht einfach ohne weitere Untersuchung durchgehen, was mich zu meiner Frage führt. Was sind die Gefahren, die mit dem Erhöhen des Stapels auf eine so große Größe verbunden sind (was könnte schief gehen), und welche Vorkehrungen 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;
    }
}
228
Sam

Beim Vergleich des Testcodes mit Sam stellte ich fest, dass wir beide Recht haben!
Über verschiedene Dinge:

  • Der Zugriff auf den Speicher (Lesen und Schreiben) ist genau so schnell , wie er ist - Stack, Global oder Heap.
  • Das Zuweisen ist jedoch auf dem Stapel am schnellsten und auf dem Haufen am langsamsten.

Es geht so: stack <global <heap. (Belegungszeit)
Eigentlich 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:

  1. Wenn Sie häufig Arrays erstellen müssen, die die Funktion nie verlassen (z. B. indem Sie ihre Referenz übergeben), ist die Verwendung des Stacks eine enorme Verbesserung.
  2. Wenn Sie ein Array recyceln können, tun Sie dies, wann immer Sie können! Der Haufen ist der beste Ort für die langfristige Aufbewahrung von Objekten. (Verschmutzung des globalen Speichers ist nicht nett; Stapelrahmen können verschwinden)

( Hinweis : 1. Gilt nur für Werttypen; Referenztypen werden auf dem Heap zugewiesen und der Vorteil wird auf 0 reduziert.)

Um die Frage selbst zu beantworten: Ich habe bei keinem Großstapeltest ein Problem festgestellt.
Ich glaube, die einzigen möglichen Probleme sind ein Stapelüberlauf, wenn Sie mit Ihren Funktionsaufrufen nicht vorsichtig sind und beim Erstellen Ihrer Threads nicht genügend Arbeitsspeicher zur Verfügung steht, wenn das System knapp wird.

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 (benötigt 120% der Zeit) für die Verwendung in Arrays!

Dies ist mein Testcode und dies ist eine Beispielausgabe:

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.

Ich habe unter Windows 8.1 Pro (mit Update 1) mit einem i7 4700 MQ unter .NET 4.5.1 getestet
Ich habe sowohl mit x86 als auch mit x64 getestet und die Ergebnisse sind identisch.

Bearbeiten : Ich habe die Stapelgröße aller Threads auf 201 MB erhöht, die Stichprobengröße auf 50 Millionen und die Iterationen auf 5 verringert.
Die Ergebnisse sind die gleichen wie oben :

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.

Allerdings scheint der Stack tatsächlich langsamer zu werden .

45
Vercas

Ich habe eine Steigerung der Verarbeitungsgeschwindigkeit um 530% festgestellt!

Das ist mit Abstand die größte Gefahr, die ich sagen würde. An Ihrem Benchmark stimmt etwas nicht, Code, der sich unvorhersehbar verhält und normalerweise irgendwo einen bösen Bug versteckt.

Es ist sehr, sehr schwierig, in einem .NET-Programm viel Stapelspeicher zu belegen, 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 Menge Adressraum, der niemals verwendet wird. Es gibt natürlich keinen Mechanismus, der eine Leistungssteigerung erklären kann, wenn der Speicher nicht verwendet wird.

Dies unterscheidet sich von einem nativen Programm, das insbesondere in C geschrieben ist, und es kann auch Platz für Arrays auf dem Stapelrahmen reservieren. Der grundlegende Malware-Angriffsvektor hinter Stapelpufferüberläufen. Möglicherweise müssen Sie auch in C # das Schlüsselwort stackalloc verwenden. Wenn Sie dies tun, besteht die offensichtliche Gefahr darin, unsicheren Code zu schreiben, der solchen Angriffen ausgesetzt ist, sowie eine zufällige Stapelrahmenbeschädigung. 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 dem Stack-Frame abzulegen 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, falls 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 erreichen, muss Editbin.exe mit der Option/STACK in einem Postbuild-Ereignis ausgeführt werden. Normalerweise können Sie bis zu 500 MB anfordern, bevor Ihr Programm im 32-Bit-Modus nicht gestartet werden kann. Threads können auch viel einfacher sein. Die Gefahrenzone für ein 32-Bit-Programm liegt normalerweise bei etwa 90 MB. Wird ausgelöst, wenn Ihr Programm längere Zeit ausgeführt wurde und der Adressraum aus früheren Zuordnungen fragmentiert wurde. Die Gesamtauslastung des Adressraums muss bereits über einen Gig hoch sein, um diesen Fehlermodus zu erhalten.

Überprüfe deinen Code dreimal, da stimmt etwas nicht. Sie können mit einem größeren Stapel keine x5-Beschleunigung erzielen, wenn Sie nicht explizit Ihren Code schreiben, um davon zu profitieren. Wofür immer unsicherer Code erforderlich ist. Das Verwenden von Zeigern in C # hat immer ein Händchen für das Erstellen von schnellerem Code, es wird nicht den Array-Begrenzungsprüfungen unterzogen.

28
Hans Passant

Ich hätte dort einen Vorbehalt, dass ich einfach nicht wissen würde, wie ich ihn vorhersagen soll - Berechtigungen, GC (die den Stapel scannen müssen) usw. - alles könnte betroffen sein. Ich wäre sehr versucht, stattdessen nicht verwalteten Speicher zu verwenden:

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

Eine Sache, die schief gehen kann, ist, dass Sie möglicherweise nicht die Erlaubnis erhalten, dies zu tun. Sofern das Framework nicht im vertrauenswürdigen Modus ausgeführt wird, ignoriert es nur die Anforderung einer größeren Stapelgröße (siehe MSDN unter Thread Constructor (ParameterizedThreadStart, Int32)).

Anstatt die Systemstapelgröße auf solch große Zahlen zu erhöhen, würde ich vorschlagen, Ihren Code so umzuschreiben, dass Iteration und eine manuelle Stapelimplementierung auf dem Heap verwendet werden.

8
PMF

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 an oder Caliper, die ausgezeichnet sind, leider nach meinem besten Wissen bietet C # nichts, was sich diesen annähert. Jon Skeet schrieb this hier, von dem ich blindlings annehme, dass er sich um die wichtigsten Dinge kümmert (Jon weiß) was er in diesem Bereich macht, auch ja, keine Sorge, 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 es in 5 Sekunden ausführen könnte, also ist höher besser.

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 (ja, das ist immer noch irgendwie traurig):

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)

Dies ergibt eine wesentlich 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 dies realistisch als Worst-Case-Szenario.) Die x86-Ergebnisse sind jedoch interessant - nicht ganz klar, was dort vor sich geht.

und hier ist der 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

Auf die Arrays mit hoher Leistung kann auf die gleiche Weise zugegriffen werden wie auf ein normales C # 1-Array, dies könnte jedoch den Beginn von Problemen bedeuten: Betrachten Sie den folgenden Code:

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

Sie erwarten eine Ausnahmebedingung "out of bound", und dies ist völlig sinnvoll, da Sie versuchen, auf Element 200 zuzugreifen, der maximal zulässige Wert jedoch 99 beträgt Folgendes zeigt keine Ausnahme:

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

Oben weisen Sie genug Speicher zu, um 100 Floats aufzunehmen, und Sie legen die Größe des (Float-) Speicherorts fest, die an der Stelle beginnt, an der dieser Speicher gestartet wurde reservierter Speicher für die Schwimmer und niemand würde wissen, was in dieser Adresse gespeichert werden könnte. Wenn Sie Glück haben, haben Sie möglicherweise nicht genutzten Speicher verwendet. Gleichzeitig können Sie jedoch möglicherweise einen Speicherort überschreiben, der zum Speichern anderer Variablen verwendet wurde. Zusammenfassend: Unvorhersehbares Laufzeitverhalten.

6
MHOOS

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:

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

Wir können die Verwendung der Anweisung und, was noch wichtiger ist, die Ausnahme, die sie auslösen, überprüfen ECMA-Spezifikation :

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

Ausnahmen, die es auslöst:

System.NullReferenceException

Und

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

Ausnahme wirft es:

System.NullReferenceException
System.IndexOutOfRangeException
System.ArrayTypeMismatchException

Wie Sie sehen können, funktioniert stelem bei der Array-Bereichsprüfung und der Typprüfung besser. Da der Schleifenkörper wenig tut (nur Wert zuweisen), dominiert der Overhead der Prüfung die Berechnungszeit. Deshalb unterscheidet sich die Leistung um 530%.

Und dies beantwortet auch Ihre Fragen: Die Gefahr besteht darin, dass keine Überprüfung der Array-Reichweite und des Typs erfolgt. Dies ist unsicher (wie in der Funktionsdeklaration angegeben; D).

5
HKTonyLee

EDIT: (kleine Änderungen im Code und in der Messung führen zu großen Änderungen im Ergebnis)

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 nicht mit unseren Messungen in Konflikt gerät. Ich habe alle Methoden veranlasst, ein letztes Element im Array zurückzugeben, und das Array ist unterschiedlich gefüllt. Es gibt auch eine zusätzliche Null in OPs TestMethod2 das macht es immer zehnmal langsamer.

Zusätzlich zu den beiden von Ihnen bereitgestellten Methoden habe ich einige andere ausprobiert. Methode 3 hat den gleichen Code wie Ihre Methode 2, aber die Funktion wird als unsafe deklariert. Methode 4 verwendet Zeigerzugriff auf ein regelmäßig erstelltes Array. Methode 5 verwendet Zeigerzugriff auf nicht verwalteten Speicher, wie von Marc Gravell beschrieben. Alle fünf Methoden werden in sehr ähnlichen Zeiten ausgeführt. M5 ist am schnellsten (und M1 ist knapp an zweiter Stelle). Der Unterschied zwischen dem schnellsten und dem langsamsten ist ungefähr 5%, was mich nicht interessieren würde.

    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