Jakie są niebezpieczeństwa podczas tworzenia wątku o rozmiarze stosu 50x domyślnym?

228

Obecnie pracuję nad programem o krytycznym znaczeniu dla wydajności i jedną ze ścieżek, którą postanowiłem zbadać, która może pomóc w zmniejszeniu zużycia zasobów, było zwiększenie stosu wątków roboczych, aby móc przenieść większość danych float[], do których będę uzyskiwać dostęp stos (za pomocą stackalloc).

Mam przeczytać , że domyślny rozmiar stosu dla wątku jest 1 MB, tak aby przenieść wszystkie moje float[]s musiałbym rozwinąć stos o około 50 razy (do 50 MB ~).

Rozumiem, że jest to ogólnie uważane za „niebezpieczne” i nie jest zalecane, ale po porównaniu mojego obecnego kodu z tą metodą odkryłem wzrost prędkości przetwarzania o 530% ! Nie mogę więc po prostu pominąć tej opcji bez dalszego dochodzenia, co prowadzi mnie do mojego pytania; jakie niebezpieczeństwa wiążą się ze zwiększeniem stosu do tak dużego rozmiaru (co może pójść nie tak) i jakie środki ostrożności powinienem podjąć, aby zminimalizować takie niebezpieczeństwa?

Mój kod testowy,

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
źródło
98
+1. Poważnie. Pytasz, co WYGLĄDA Jak idiotyczne pytanie poza normą, a następnie robisz BARDZO dobry przypadek, że w twoim konkretnym scenariuszu rozsądnie jest rozważyć, ponieważ odrobiłeś pracę domową i zmierzyłeś wynik. To BARDZO dobre - tęsknię za wieloma pytaniami. Bardzo fajnie - dobrze, że rozważasz coś takiego, niestety wielu programistów C # nie jest świadomych tych możliwości optymalizacji. Tak, często niepotrzebne - ale czasem jest to krytyczne i robi ogromną różnicę.
TomTom
5
Chciałbym zobaczyć dwa kody, które mają 530% różnicy w szybkości przetwarzania, wyłącznie ze względu na przenoszenie tablicy na stos. To po prostu nie wydaje się właściwe.
Dialecticus,
13
Zanim zeskoczysz na tę drogę: czy próbowałeś Marshal.AllocHGlobal(nie zapomnij FreeHGlobalteż) przydzielić danych poza pamięć zarządzaną? Następnie rzuć wskaźnik na a float*i powinieneś zostać posortowany.
Marc Gravell
2
Czujesz się dobrze, jeśli wykonujesz dużo alokacji. Stackalloc omija wszystkie problemy GC, które również mogą tworzyć / tworzy bardzo silną lokalizację na poziomie procesora. To jedna z rzeczy, które wyglądają jak mikrooptymalizacje - chyba że napiszesz wysokowydajny program matematyczny i zachowujesz się dokładnie tak, a to robi różnicę;)
TomTom
6
Moje podejrzenie: jedna z tych metod uruchamia sprawdzanie granic przy każdej iteracji pętli, podczas gdy druga nie, lub jest zoptymalizowana.
pjc50

Odpowiedzi:

45

Porównując kod testowy z Samem, stwierdziłem, że oboje mamy rację!
Jednak o różnych rzeczach:

  • Dostęp do pamięci (odczyt i zapis) jest równie szybki, gdziekolwiek się znajduje - stos, globalnie lub na stosie.
  • Przydział ten jest jednak najszybszy na stosie, a najwolniejszy na stosie.

To idzie tak: stack< global< heap. (czas alokacji)
Technicznie rzecz biorąc, alokacja stosu nie jest tak naprawdę alokacją, środowisko wykonawcze po prostu upewnia się, że część stosu (ramka?) jest zarezerwowana dla tablicy.

Jednak zdecydowanie radzę zachować ostrożność.
Polecam następujące:

  1. Gdy trzeba często tworzyć tablice, które nigdy nie opuszczają funkcji (np. Przekazując odwołanie), użycie stosu będzie ogromną poprawą.
  2. Jeśli możesz przetworzyć tablicę, rób to, kiedy tylko możesz! Kupa jest najlepszym miejscem do długoterminowego przechowywania obiektów. (zanieczyszczanie pamięci globalnej nie jest miłe; ramki stosu mogą zniknąć)

( Uwaga : 1. dotyczy tylko typów wartości; typy referencyjne zostaną przypisane do stosu, a korzyść zostanie zmniejszona do 0)

Aby odpowiedzieć na pytanie: w ogóle nie napotkałem żadnego problemu z testem na dużym stosie.
Uważam, że jedynymi możliwymi problemami są przepełnienie stosu, jeśli nie jesteś ostrożny przy wywoływaniu funkcji i wyczerpaniu się pamięci podczas tworzenia wątków, jeśli w systemie zaczyna brakować.

Poniższa sekcja to moja wstępna odpowiedź. To źle, a testy nie są poprawne. Jest przechowywany tylko w celach informacyjnych.


Mój test wskazuje, że pamięć alokowana na stosie i pamięć globalna są co najmniej 15% wolniejsze niż (zajmuje 120% czasu) pamięci alokowanej na stosie do użycia w tablicach!

To jest mój kod testowy , a to przykładowe wyjście:

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.

Testowałem na Windows 8.1 Pro (z aktualizacją 1), używając i7 4700 MQ, pod .NET 4.5.1
Testowałem zarówno z x86, jak i x64, a wyniki są identyczne.

Edycja : Zwiększyłem rozmiar stosu wszystkich wątków o 201 MB, wielkość próbki do 50 milionów i zmniejszyłem iteracje do 5.
Wyniki są takie same jak powyżej :

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.

Wygląda jednak na to, że stos jest coraz wolniejszy .

Vercas
źródło
Musiałbym się nie zgodzić, zgodnie z wynikami mojego testu porównawczego (wyniki w komentarzu na dole strony) pokazują, że stos jest nieznacznie szybszy niż globalny i znacznie szybszy niż sterta; i aby mieć absolutną pewność, że moje wyniki są dokładne, przeprowadziłem test 20 razy, a każda metoda została wywołana 100 razy na iterację testu. Czy zdecydowanie poprawnie uruchamiasz swój test porównawczy?
Sam
Otrzymuję bardzo niespójne wyniki. Z pełnym zaufaniem, x64, konfiguracja wydania, bez debuggera, wszystkie są równie szybkie (różnica mniejsza niż 1%; fluktuacja), podczas gdy twoje jest znacznie szybsze ze stosem. Muszę dalej testować! Edycja : Twój POWINIEN rzucić wyjątek przepełnienia stosu. Po prostu przydzielasz wystarczającą ilość dla tablicy. O_o
Vercas
Tak, wiem, jest blisko. Musisz powtórzyć testy kilka razy, tak jak ja, może spróbuj wziąć średnio ponad 5 biegów.
Sam
1
@ Voo Pierwsze uruchomienie zajęło mi tyle samo czasu, co 100 uruchomienie dowolnego testu. Z mojego doświadczenia wynika, że ​​ta Java JIT w ogóle nie dotyczy .NET. Jedyną „rozgrzewką”, jaką wykonuje .NET, jest ładowanie klas i zestawów, gdy są używane po raz pierwszy.
Vercas
2
@Voo Przetestuj mój test porównawczy i ten z treści, którą dodał w komentarzu do tej odpowiedzi. Zbierz kody razem i przeprowadź kilkaset testów. Następnie wróć i zgłoś swój wniosek. Testy wykonałem bardzo dokładnie i bardzo dobrze wiem, o czym mówię, mówiąc, że .NET nie interpretuje żadnego kodu bajtowego, tak jak Java, natychmiast JIT.
Vercas
28

Odkryłem wzrost prędkości przetwarzania o 530%!

To zdecydowanie największe niebezpieczeństwo, jakie powiedziałbym. Jest coś poważnie nie tak z twoim testem porównawczym, kod, który zachowuje się tak nieprzewidywalnie, zwykle ma gdzieś ukryty paskudny błąd.

Bardzo, bardzo trudno jest zużyć dużo miejsca na stosie w programie .NET, poza nadmierną rekurencją. Rozmiar ramy stosu metod zarządzanych jest ustalony w kamieniu. Po prostu suma argumentów metody i zmiennych lokalnych w metodzie. Pomijając te, które mogą być przechowywane w rejestrze procesora, możesz to zignorować, ponieważ jest ich tak mało.

Zwiększenie rozmiaru stosu niczego nie osiąga, po prostu zarezerwujesz garść przestrzeni adresowej, która nigdy nie zostanie wykorzystana. Nie ma żadnego mechanizmu, który mógłby wyjaśnić wzrost wydajności w wyniku nieużywania pamięci.

W przeciwieństwie do programu rodzimego, szczególnie napisanego w C, może również zarezerwować miejsce dla tablic na ramce stosu. Podstawowy wektor ataku złośliwego oprogramowania za przepełnienie bufora stosu. Możliwe również w C #, musisz użyć stackallocsłowa kluczowego. Jeśli to robisz, oczywistym niebezpieczeństwem jest napisanie niebezpiecznego kodu, który podlega takim atakom, a także losowe uszkodzenie ramki stosu. Bardzo trudno zdiagnozować błędy. Wydaje mi się, że w późniejszych jitterach istnieje przeciwdziałanie, począwszy od .NET 4.0, gdzie jitter generuje kod, aby umieścić „cookie” w ramce stosu i sprawdza, czy jest on nadal nienaruszony po powrocie metody. Natychmiastowa awaria na pulpicie bez możliwości przechwycenia lub zgłoszenia nieszczęścia, jeśli tak się stanie. To ... niebezpieczne dla stanu psychicznego użytkownika.

Główny wątek twojego programu, ten uruchomiony przez system operacyjny, będzie miał domyślnie 1 MB stosu, 4 MB podczas kompilacji programu ukierunkowanego na x64. Zwiększenie tego wymaga uruchomienia Editbin.exe z opcją / STACK w zdarzeniu po kompilacji. Zwykle możesz poprosić o maksymalnie 500 MB, zanim Twój program będzie miał problemy z uruchomieniem podczas pracy w trybie 32-bitowym. Wątki mogą, o wiele łatwiej, oczywiście, strefa zagrożenia zwykle waha się wokół 90 MB dla programu 32-bitowego. Wywoływane, gdy program działa od dłuższego czasu, a przestrzeń adresowa została podzielona z poprzednich przydziałów. Aby uzyskać ten tryb awarii, całkowite wykorzystanie przestrzeni adresowej musi być już wysokie, ponad gig.

Potrójnie sprawdź kod, coś jest nie tak. Nie możesz uzyskać przyspieszenia x5 z większym stosem, chyba że wprost napiszesz swój kod, aby z niego skorzystać. Co zawsze wymaga niebezpiecznego kodu. Używanie wskaźników w języku C # zawsze ma talent do tworzenia szybszego kodu, nie podlega kontroli granic tablicy.

Hans Passant
źródło
21
Zgłoszone 5-krotne przyspieszenie pochodziło z przejścia z float[]do float*. Duży stos był po prostu sposobem na osiągnięcie tego. Przyspieszenie x5 w niektórych scenariuszach jest całkowicie uzasadnione dla tej zmiany.
Marc Gravell
3
Okej, nie miałem jeszcze fragmentu kodu, kiedy zacząłem odpowiadać na pytanie. Nadal wystarczająco blisko.
Hans Passant
22

Miałbym zastrzeżenie, że po prostu nie wiedziałbym, jak to przewidzieć - uprawnienia, GC (które muszą skanować stos) itp. - wszystko to może mieć wpływ. Chciałbym zamiast tego użyć niezarządzanej pamięci:

var ptr = Marshal.AllocHGlobal(sizeBytes);
try
{
    float* x = (float*)ptr;
    DoWork(x);
}
finally
{
    Marshal.FreeHGlobal(ptr);
}
Marc Gravell
źródło
1
Pytanie poboczne: Dlaczego GC miałby skanować stos? Przydzielona pamięć przez stackallocnie podlega śmieciu.
dcastro
6
@dcastro musi zeskanować stos, aby sprawdzić odniesienia, które istnieją tylko na stosie. Po prostu nie wiem, co zrobi, gdy dojdzie do tak ogromnego stackalloc- to musi trochę przeskoczyć i masz nadzieję, że zrobi to bez wysiłku - ale chcę, aby to wprowadziło niepotrzebne komplikacje / obawy. IMO stackallocświetnie nadaje się jako bufor na zarysowania, ale w przypadku dedykowanego obszaru roboczego bardziej prawdopodobne jest, że po prostu alokuje gdzieś fragment pamięci zamiast nadużywać / dezorientować stosu
Marc Gravell
8

Jedną z rzeczy, które mogą pójść nie tak, jest to, że możesz nie uzyskać na to pozwolenia. O ile nie działa w trybie pełnego zaufania, Framework po prostu zignoruje żądanie większego rozmiaru stosu (patrz MSDN na Thread Constructor (ParameterizedThreadStart, Int32))

Zamiast zwiększać rozmiar stosu systemowego do tak ogromnych liczb, sugerowałbym przepisanie kodu, aby używał iteracji i ręcznej implementacji stosu na stercie.

PMF
źródło
1
Dobry pomysł, zamiast tego powtórzę. Poza tym mój kod działa w trybie pełnego zaufania, więc czy są jeszcze inne rzeczy, na które powinienem zwrócić uwagę?
Sam
6

Tablice o wysokiej wydajności mogą być dostępne w taki sam sposób jak normalne C #, ale może to być początek problemów: Rozważ następujący kod:

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

Oczekujesz wyjątku spoza zakresu, co ma sens, ponieważ próbujesz uzyskać dostęp do elementu 200, ale maksymalna dozwolona wartość to 99. Jeśli przejdziesz do trasy stackalloc, wokół tablicy nie będzie zawijany żaden obiekt następujące nie pokaże żadnego wyjątku:

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

Powyżej przydzielasz wystarczającą ilość pamięci, aby pomieścić 100 liczb zmiennoprzecinkowych i ustawiasz lokalizację pamięci sizeof (zmiennoprzecinkową), która zaczyna się w miejscu początkowym tej pamięci + 200 * sizeof (zmiennoprzecinkowa), aby utrzymać wartość zmiennoprzecinkową 10. Nic dziwnego, że ta pamięć znajduje się poza przydzielona pamięć dla pływaków i nikt nie wiedziałby, co można przechowywać pod tym adresem. Jeśli masz szczęście, mogłeś użyć trochę nieużywanej pamięci, ale jednocześnie możesz zastąpić niektóre miejsca, które były używane do przechowywania innych zmiennych. Podsumowując: nieprzewidywalne zachowanie w środowisku wykonawczym.

MHOOS
źródło
Faktycznie źle. Testy środowiska wykonawczego i kompilatora nadal tam są.
TomTom
9
@TomTom erm, no; odpowiedź ma wartość; pytanie mówi o tym stackalloc, w którym przypadku mówimy float*itp. - który nie ma takich samych kontroli. Nazywa się to unsafez bardzo dobrego powodu. Osobiście cieszę się, że mogę z niego skorzystać, unsafegdy jest ku temu dobry powód, ale Sokrates podaje kilka rozsądnych argumentów.
Marc Gravell
@Marc Dla pokazanego kodu (po uruchomieniu JIT) nie ma już sprawdzania granic, ponieważ kompilator nie jest w stanie uzasadnić, że wszystkie dostępy są ograniczone. Zasadniczo może to jednak mieć znaczenie.
Voo
6

Języki znakowania Microbench z JIT i GC, takie jak Java lub C #, mogą być nieco skomplikowane, więc ogólnie dobrym pomysłem jest użycie istniejącego frameworka - Java oferuje mhf lub Caliper, które są doskonałe, niestety według mojej najlepszej wiedzy C # cokolwiek do nich zbliża się. Jon Skeet napisał to tutaj, o którym ślepo założę, że zajmuje się najważniejszymi rzeczami (Jon wie, co robi w tej dziedzinie; również tak, nie martw się, sprawdziłem). Ulepszyłem nieco czas, ponieważ 30 sekund na test po rozgrzewce było zbyt duże dla mojej cierpliwości (powinno to wystarczyć 5 sekund).

Najpierw więc wyniki .NET 4.5.1 pod Windows 7 x64 - liczby oznaczają iteracje, które mógłby uruchomić w 5 sekund, więc im wyższa, tym lepiej.

JIT x64:

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 (tak, to wciąż trochę smutne):

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)

Daje to znacznie bardziej rozsądne przyspieszenie o najwyżej 14% (a większość kosztów ogólnych wynika z konieczności uruchomienia GC, uważaj realistycznie za najgorszy scenariusz). Wyniki x86 są jednak interesujące - nie do końca jasne, co się tam dzieje.

a oto kod:

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
źródło
Ciekawe spostrzeżenie, będę musiał ponownie sprawdzić moje testy porównawcze. Chociaż to tak naprawdę nie odpowiada na moje pytanie: „ ... jakie są zagrożenia związane ze zwiększeniem stosu do tak dużego rozmiaru ... ”. Nawet jeśli moje wyniki są niepoprawne, pytanie jest nadal aktualne; Mimo wszystko doceniam ten wysiłek.
Sam
1
@Sam Podczas używania 12500000jako rozmiaru faktycznie otrzymuję wyjątek przepełnienia stosu . Ale głównie chodziło o odrzucenie podstawowej przesłanki, że użycie kodu przydzielonego do stosu jest o kilka rzędów wielkości szybsze. W przeciwnym razie wykonujemy prawie najmniej pracy, a różnica wynosi już tylko około 10-15% - w praktyce będzie jeszcze niższa. To moim zdaniem zdecydowanie zmienia całą dyskusję.
Voo
5

Ponieważ różnica w wydajności jest zbyt duża, problem nie jest prawie związany z alokacją. Jest to prawdopodobnie spowodowane dostępem do tablicy.

Zdemontowałem korpus pętli funkcji:

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

Możemy sprawdzić użycie instrukcji i, co ważniejsze, wyjątek, który wprowadzają w specyfikacji ECMA :

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

Wyjątki, które rzuca:

System.NullReferenceException

I

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

Wyjątek stanowi:

System.NullReferenceException
System.IndexOutOfRangeException
System.ArrayTypeMismatchException

Jak widać, stelemwięcej działa przy sprawdzaniu zakresu tablic i sprawdzaniu typów. Ponieważ ciało pętli robi niewiele (tylko przypisuje wartość), narzut związany z sprawdzaniem dominuje czas obliczeń. Właśnie dlatego wydajność różni się o 530%.

To także odpowiada na twoje pytania: niebezpieczeństwo polega na braku sprawdzania zasięgu i typu tablicy. Jest to niebezpieczne (jak wspomniano w deklaracji funkcji; D).

HKTonyLee
źródło
4

EDYCJA: (mała zmiana kodu i pomiaru powoduje dużą zmianę wyniku)

Najpierw uruchomiłem zoptymalizowany kod w debuggerze (F5), ale to było złe. Powinien być uruchamiany bez debuggera (Ctrl + F5). Po drugie, kod może zostać gruntownie zoptymalizowany, więc musimy go skomplikować, aby optymalizator nie zadziałał z naszym pomiarem. Sprawiłem, że wszystkie metody zwracają ostatni element w tablicy, a tablica jest zapełniona inaczej. Jest też dodatkowe zero w OP, TestMethod2które zawsze sprawia, że ​​jest dziesięć razy wolniejsze.

Próbowałem kilka innych metod, oprócz dwóch, które podałeś. Metoda 3 ma taki sam kod jak metoda 2, ale funkcja jest zadeklarowana unsafe. Metoda 4 wykorzystuje dostęp do wskaźnika do regularnie tworzonej tablicy. Metoda 5 polega na wykorzystaniu dostępu wskaźnika do niezarządzanej pamięci, jak opisał Marc Gravell. Wszystkie pięć metod działa w bardzo podobnych czasach. M5 jest najszybszy (a M1 jest na drugim miejscu). Różnica między najszybszym a najwolniejszym wynosi około 5%, co mnie nie obchodzi.

    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);
        }
    }
Dialektyczny
źródło
Czy M3 jest tym samym co M2 oznaczone tylko „niebezpiecznym”? Raczej podejrzane, że będzie szybciej ... jesteś pewien?
Roman Starkov
@romkyns Właśnie uruchomiłem test porównawczy (M2 vs M3) i, co zaskakujące, M3 jest w rzeczywistości 2,14% szybszy niż M2.
Sam
Wniosek jest taki, że użycie stosu nie jest potrzebne. Przydzielając duże bloki, takie jak podałem w poście, zgadzam się, ale po ukończeniu kilku kolejnych testów porównawczych M1 vs M2 (używając pomysłu PFM dla obu metod) z pewnością bym to zrobił muszę się nie zgodzić, ponieważ M1 jest teraz 135% szybszy niż M2.
Sam
1
@Sam Ale nadal porównujesz dostęp do wskaźnika z dostępem do tablicy! ŻE jest primarly co czyni go szybciej. TestMethod4vs TestMethod1to znacznie lepsze porównanie dla stackalloc.
Roman Starkov,
@romkyns Ah tak, dobra uwaga, zapomniałem o tym; Ponownie uruchomiłem testy , różnica wynosi teraz tylko 8% (M1 jest najszybszy z tych dwóch).
Sam