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

228

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;
    }
}
Sam
quelle
98
+1. Ernsthaft. Sie fragen, was wie eine idiotische Frage aus der Norm aussieht, und dann machen Sie einen SEHR guten Fall, dass es in Ihrem speziellen Szenario sinnvoll ist, darüber nachzudenken, weil Sie Ihre Hausaufgaben gemacht und das Ergebnis gemessen haben. Das ist SEHR gut - das vermisse ich bei vielen Fragen. Sehr schön - gut, dass Sie so etwas in Betracht ziehen. Leider sind sich viele, viele C # -Programmierer dieser Optimierungsmöglichkeiten nicht bewusst. Ja, oft nicht benötigt - aber manchmal ist es kritisch und macht einen großen Unterschied.
TomTom
5
Ich bin daran interessiert, die beiden Codes zu sehen, die einen Unterschied von 530% in der Verarbeitungsgeschwindigkeit aufweisen, allein aufgrund des Verschiebens des Arrays zum Stapel. Das fühlt sich einfach nicht richtig an.
Dialecticus
13
Bevor Sie diesen Weg beschreiten: Haben Sie versucht Marshal.AllocHGlobal(nicht zu vergessen FreeHGlobal), die Daten außerhalb des verwalteten Speichers zuzuweisen ? Setzen Sie dann den Zeiger auf a float*, und Sie sollten sortiert sein.
Marc Gravell
2
Es fühlt sich richtig an, wenn Sie viele Zuweisungen vornehmen. Stackalloc umgeht alle GC-Probleme, die auch auf Prozessorebene zu einer sehr starken Lokalität führen können. Dies ist eines der Dinge, die wie Mikrooptimierungen aussehen - es sei denn, Sie schreiben ein hochleistungsfähiges mathematisches Programm und haben genau dieses Verhalten und es macht einen Unterschied;)
TomTom
6
Mein Verdacht: Eine dieser Methoden löst bei jeder Schleifeniteration eine Grenzüberprüfung aus, während die andere dies nicht tut, oder sie wird wegoptimiert.
pjc50

Antworten:

45

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

  • Der Zugriff auf Speicher (Lesen und Schreiben) ist überall genauso schnell - Stack, Global oder Heap.
  • Die Zuweisung ist jedoch auf dem Stapel am schnellsten und auf dem Heap am langsamsten.

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:

  1. Wenn Sie häufig Arrays erstellen müssen, die die Funktion nie verlassen (z. B. durch Übergeben der Referenz), ist die Verwendung des Stapels eine enorme Verbesserung.
  2. Wenn Sie ein Array recyceln können, tun Sie dies, wann immer Sie können! Der Heap ist der beste Ort für die langfristige Objektspeicherung. (Verschmutzen des globalen Speichers ist nicht schön; Stapelrahmen können verschwinden)

( 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:

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) 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 :

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.

Es scheint jedoch, dass der Stapel tatsächlich langsamer wird .

Vercas
quelle
Laut den Ergebnissen meines Benchmarks (Ergebnisse siehe Kommentar unten auf der Seite) muss ich nicht zustimmen, dass der Stack geringfügig schneller als global und viel schneller als der Heap ist. und um sicherzugehen, dass meine Ergebnisse korrekt sind, wurde der Test 20 Mal ausgeführt, und jede Methode wurde 100 Mal pro Testiteration aufgerufen. Führen Sie Ihren Benchmark definitiv richtig aus?
Sam
Ich erhalte sehr inkonsistente Ergebnisse. Mit vollem Vertrauen, x64, Release-Konfiguration und keinem Debugger sind sie alle gleich schnell (weniger als 1% Unterschied; schwankend), während Ihre mit einem Stack tatsächlich viel schneller ist. Ich muss weiter testen! Bearbeiten : Ihre SOLLTE eine Stapelüberlauf-Ausnahme auslösen. Sie weisen lediglich genug für das Array zu. O_o
Vercas
Ja, ich weiß, es ist nah. Sie müssen die Benchmarks ein paar Mal wiederholen, wie ich es getan habe. Vielleicht versuchen Sie, einen Durchschnitt von ungefähr 5 Läufen zu erreichen.
Sam
1
@Voo Der erste Lauf hat für mich genauso viel Zeit in Anspruch genommen wie der 100. Lauf eines Tests. Nach meiner Erfahrung gilt diese Java JIT-Sache überhaupt nicht für .NET. Das einzige "Aufwärmen", das .NET durchführt, ist das Laden von Klassen und Assemblys bei der ersten Verwendung.
Vercas
2
@Voo Teste meinen Benchmark und den aus dem Kern, den er in einem Kommentar zu dieser Antwort hinzugefügt hat. Stellen Sie die Codes zusammen und führen Sie einige hundert Tests durch. Dann komm zurück und berichte über deine Schlussfolgerung. Ich habe meine Tests sehr gründlich durchgeführt und weiß sehr gut, wovon ich spreche, wenn ich sage, dass .NET keinen Bytecode wie Java interpretiert, sondern ihn sofort JITs.
Vercas
28

Ich habe eine Steigerung der Verarbeitungsgeschwindigkeit um 530% festgestellt!

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 stackallocSchlü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.

Hans Passant
quelle
21
Die gemeldete 5-fache Beschleunigung erfolgte durch Umstellung von float[]auf float*. Der große Stapel war einfach, wie das erreicht wurde. Eine x5-Beschleunigung in einigen Szenarien ist für diese Änderung durchaus sinnvoll.
Marc Gravell
3
Okay, ich hatte das Code-Snippet noch nicht, als ich anfing, die Frage zu beantworten. Immer noch nah genug.
Hans Passant
22

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:

var ptr = Marshal.AllocHGlobal(sizeBytes);
try
{
    float* x = (float*)ptr;
    DoWork(x);
}
finally
{
    Marshal.FreeHGlobal(ptr);
}
Marc Gravell
quelle
1
Nebenfrage: Warum sollte der GC den Stapel scannen müssen? Der von zugewiesene Speicher stackallocunterliegt keiner Speicherbereinigung .
Dcastro
6
@dcastro Der Stapel muss gescannt werden, um nach Referenzen zu suchen, die nur auf dem Stapel vorhanden sind. Ich weiß einfach nicht, was es tun wird, wenn es so groß wird 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. IMO stackalloceignet 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.
Marc Gravell
8

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.

PMF
quelle
1
Gute Idee, ich werde stattdessen durchgehen. Außerdem wird mein Code im Vertrauensmodus ausgeführt. Gibt es also noch andere Dinge, auf die ich achten sollte?
Sam
6

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:

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

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:

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

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.

MHOOS
quelle
Faktisch falsch. Die Laufzeit- und Compilertests sind noch vorhanden.
TomTom
9
@ TomTom erm, nein; die Antwort hat Verdienst; Die Frage spricht darüber stackalloc, in welchem ​​Fall wir über float*usw. sprechen - die nicht die gleichen Prüfungen hat. Es hat unsafeeinen sehr guten Grund. Persönlich bin ich sehr glücklich, unsafewenn es einen guten Grund gibt, aber Sokrates macht einige vernünftige Punkte.
Marc Gravell
@Marc Für den angezeigten Code (nachdem die JIT ausgeführt wurde) gibt es keine Grenzüberprüfungen mehr, da es für den Compiler trivial ist, zu begründen, dass alle Zugriffe In-Bounds sind. Im Allgemeinen kann dies jedoch sicherlich einen Unterschied machen.
Voo
6

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:

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 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 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:

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);
}
Voo
quelle
Eine interessante Beobachtung, ich muss meine Benchmarks noch einmal überprüfen. Obwohl dies meine Frage immer noch nicht wirklich beantwortet: " ... was sind die Gefahren, die mit der Vergrößerung des Stapels auf eine so große Größe verbunden sind ... ". Auch wenn meine Ergebnisse falsch sind, ist die Frage immer noch gültig; Ich schätze die Mühe trotzdem.
Sam
1
@Sam Bei Verwendung 12500000als 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.
Voo
5

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 vor allem die Ausnahme überprüfen, die sie in der ECMA-Spezifikation auslösen :

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

Ausnahmen wirft es:

System.NullReferenceException

Und

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

Ausnahme, die es wirft:

System.NullReferenceException
System.IndexOutOfRangeException
System.ArrayTypeMismatchException

Wie Sie sehen können, stelemfunktioniert 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).

HKTonyLee
quelle
4

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.

    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);
        }
    }
Dialecticus
quelle
Also ist M3 dasselbe wie M2, das nur mit "unsicher" gekennzeichnet ist? Eher verdächtig, dass es schneller gehen würde ... bist du dir sicher?
Roman
@romkyns Ich habe gerade einen Benchmark durchgeführt (M2 gegen M3), und überraschenderweise ist M3 tatsächlich 2,14% schneller als M2.
Sam
" Die Schlussfolgerung ist, dass die Verwendung des Stapels nicht erforderlich ist. " Wenn ich große Blöcke zuweise, wie ich sie in meinem Beitrag angegeben habe, stimme ich zu, aber nachdem ich gerade einige weitere Benchmarks M1 gegen M2 abgeschlossen habe (unter Verwendung der Idee von PFM für beide Methoden), würde ich dies sicherlich tun muss nicht zustimmen, da M1 jetzt 135% schneller ist als M2.
Sam
1
@ Sam Aber Sie vergleichen immer noch den Zeigerzugriff mit dem Arrayzugriff! DAS ist in erster Linie das, was es schneller macht. TestMethod4vs TestMethod1ist ein viel besserer Vergleich für stackalloc.
Roman Starkov
@romkyns Ah ja guter Punkt, das habe ich vergessen; Ich habe die Benchmarks erneut ausgeführt , es gibt jetzt nur noch einen Unterschied von 8% (M1 ist der schnellere von beiden).
Sam