Jakie są zagrożenia podczas tworzenia wątku o rozmiarze stosu 50x domyślnym?

Obecnie pracuję nad bardzo krytycznym programem i jedną ścieżką, którą postanowiłem zbadać, która może pomóc zmniejszyć zużycie zasobów, było zwiększenie rozmiaru stosu moich wątków roboczych, dzięki czemu mogę przenieść większość danych (float[] s), które będę uzyskiwał na stosie (za pomocą stackalloc).

Przeczytałem, że domyślny rozmiar stosu dla wątku to 1 MB, więc aby przenieść wszystkie moje float[] s, musiałbym powiększyć stos o około 50 razy (do 50 MB~).

I 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 530% zwiększenie szybkości przetwarzania! Nie mogę więc po prostu przejść przez tę opcję bez dalszego dochodzenia, co prowadzi mnie do mojego pytania; Jakie są zagrożenia związane z zwiększeniem stosu do tak dużych rozmiarów (co może pójść źle), i jakie środki ostrożności należy podjąć, aby zminimalizować takie zagrożenia?

Mój test kod,

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;
    }
}
Author: Community, 2014-06-13

8 answers

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 tak samo szybki gdziekolwiek jest-stack, global lub heap.
  • przydzielanie jest jednak najszybsze na stosie i najwolniejsze na stosie.

It goes like this: stack global heap. (czas przydziału)
Technicznie, alokacja stosu nie jest tak naprawdę alokacją, runtime po prostu upewnia się, że część stosu(ramka?) jest zarezerwowane dla tablicy.

Zdecydowanie radzę być ostrożnym.
Polecam:
  1. kiedy trzeba często tworzyć tablice, które nigdy nie opuszczają funkcji (np. przekazując jej referencję), użycie stosu będzie ogromną poprawą.
  2. Jeśli możesz przetwarzać tablicę, zrób to, kiedy tylko możesz! Sterta jest najlepszym miejscem do długoterminowego przechowywania obiektów. (zanieczyszczanie pamięci globalnej nie jest miło; stack frames może zniknąć)

(uwaga: 1. dotyczy tylko typów wartości; typy referencyjne zostaną przydzielone na stercie, a korzyść zostanie zmniejszona do 0)

Odpowiadając na samo pytanie: nie napotkałem żadnego problemu z żadnym testem z dużym stosem.
Uważam, że jedynymi możliwymi problemami jest przepełnienie stosu, jeśli nie jesteś ostrożny z wywołaniami funkcji i brak pamięci podczas tworzenia wątków, jeśli system jest uruchomiony nisko.

Sekcja poniżej jest moją początkową odpowiedzią. Jest źle, a testy nie są poprawne. Jest przechowywany tylko w celach informacyjnych.


Mój test wskazuje, że pamięć przydzielona stosem i pamięć globalna jest co najmniej 15% wolniejsza niż (zajmuje 120% czasu) przydzielona stercie pamięci do użycia w tablicach!

To jest mój kod testowy, a to jest 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 i wyniki są identyczne.

Edit : zwiększyłem rozmiar stosu wszystkich wątków o 201 MB, wielkość próby 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.

Wydaje się jednak, że stos jest w rzeczywistości coraz wolniejszy.

 45
Author: Vercas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-05-09 11:19:56
Odkryłem 530% wzrost szybkości przetwarzania!
To zdecydowanie największe zagrożenie. Coś jest nie tak z Twoim benchmarkiem, kod, który zachowuje się tak nieprzewidywalnie, zwykle ma gdzieś ukryty paskudny błąd.

Jest bardzo, bardzo trudno pochłonąć dużo miejsca na stosie w programie. NET, poza nadmierną rekurencją. Rozmiar ramki stosu zarządzanych metod jest ustawiany w kamieniu. Po prostu suma argumentów metoda i zmienne lokalne 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 nic nie da, po prostu zarezerwujesz trochę przestrzeni adresowej, która nigdy nie zostanie użyta. Nie ma mechanizmu, który mógłby wyjaśnić wzrost perf z nieużywania pamięci oczywiście.

Jest to w przeciwieństwie do natywnego programu, szczególnie tego napisanego w C, może on również rezerwować miejsce na tablice na stack frame. Podstawowy wektor ataku złośliwego oprogramowania za przepełnieniami bufora stosu. Możliwe również w C#, trzeba by użyć słowa kluczowego stackalloc. Jeśli to robisz, oczywistym niebezpieczeństwem jest konieczność napisania niebezpiecznego kodu, który podlega takim atakom, a także uszkodzenie losowej ramki stosu. Bardzo trudne do zdiagnozowania błędy. Istnieje przeciwdziałanie temu w późniejszych treściach, myślę, że zaczynając od. NET 4.0, gdzie jitter generuje kod, aby umieścić "cookie" na ramce stosu i sprawdza, czy jest w stanie nienaruszonym po powrocie metody. Natychmiastowa awaria na pulpicie bez możliwości przechwycenia lub zgłoszenia awarii, jeśli tak się stanie. Tak ... niebezpieczne dla stanu psychicznego użytkownika.

Główny wątek programu, ten uruchomiony przez system operacyjny, będzie miał domyślnie 1 MB stosu, 4 MB podczas kompilacji programu kierującego x64. Zwiększenie tego wymaga uruchomienia Editbin.exe z opcją / STACK w zdarzeniu post build. Zazwyczaj można poprosić o maksymalnie 500 MB przed program będzie miał problemy z uruchomieniem podczas pracy w trybie 32-bitowym. Wątki też mogą, oczywiście o wiele łatwiej, strefa zagrożenia Zwykle unosi się wokół 90 MB dla programu 32-bitowego. Wywołane, gdy program był uruchomiony przez długi czas, a przestrzeń adresowa została fragmentowana z poprzednich przydziałów. Całkowite wykorzystanie przestrzeni adresowej musi być już wysokie, ponad GIGA, aby uzyskać ten tryb awarii.

Potrójne Sprawdzenie kodu, coś jest nie tak. Nie można dostać X5 speedup z większym stosuj, chyba że jawnie napiszesz swój kod, aby go wykorzystać. Który zawsze wymaga niebezpiecznego kodu. Używanie wskaźników w C# zawsze ma talent do tworzenia szybszego kodu, nie jest poddawane kontroli granic tablic.
 27
Author: Hans Passant,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-06-19 08:42:56

Chciałbym mieć tam zastrzeżenie, że po prostu nie wiem , jak to przewidzieć-uprawnienia, GC (który musi skanować stos), itp-wszystko może mieć wpływ. Bardzo bym się skusił, żeby zamiast tego użyć pamięci niezarządzanej:

var ptr = Marshal.AllocHGlobal(sizeBytes);
try
{
    float* x = (float*)ptr;
    DoWork(x);
}
finally
{
    Marshal.FreeHGlobal(ptr);
}
 22
Author: Marc Gravell,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-06-13 08:56:46

Jedną rzeczą, która może pójść źle, jest to, że możesz nie dostać pozwolenia na to. Jeśli nie działa w trybie pełnego zaufania, Framework zignoruje żądanie o większy rozmiar stosu (zobacz MSDN on Thread Constructor (ParameterizedThreadStart, Int32))

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

 8
Author: PMF,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-06-13 08:50:54

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

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

Spodziewasz się wyjątku out of bound i to ma sens, ponieważ próbujesz uzyskać dostęp do elementu 200, ale maksymalna dozwolona wartość to 99. Jeśli przejdziesz do trasy stackalloc, nie będzie żadnego obiektu owiniętego wokół tablicy, aby sprawdzić Wiązanie, a poniższe nie pokażą żadnych wyjątek:

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

Powyżej przydzielasz wystarczającą ilość pamięci do przechowywania 100 pływaków i ustawiasz lokalizację pamięci sizeof (float), która rozpoczyna się w miejscu uruchomionym tej pamięci + 200*sizeof(float) do przechowywania wartości float 10. Nic dziwnego, że pamięć ta znajduje się poza przydzieloną pamięcią dla pływaków i nikt nie wiedziałby, co może być przechowywane w tym adresie. Jeśli masz szczęście, możesz użyć obecnie nieużywanej pamięci, ale jednocześnie prawdopodobnie możesz nadpisuje lokalizację używaną do przechowywania innych zmiennych. Podsumowując: nieprzewidywalne zachowanie środowiska uruchomieniowego.

 6
Author: MHOOS,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-06-13 08:55:26

Mikrobenchmarking języków z JIT i GC, takich jak Java lub C# może być nieco skomplikowany, więc ogólnie dobrym pomysłem jest użycie istniejącego frameworka-Java oferuje mhf lub Caliper, które są doskonałe, niestety z mojej najlepszej wiedzy C# nie oferuje niczego zbliżającego się do nich. Jon Skeet napisał to Tutaj, które ślepo zakładam, zajmuje się najważniejszymi rzeczami(Jon wie, co robi w tej dziedzinie; również tak, nie martwiłem się, że faktycznie sprawdziłem). Trochę podkręciłem czas bo 30 sekund na test po rozgrzewce to za dużo jak na moją cierpliwość (5 sekund powinno wystarczyć).

Więc najpierw wyniki,. NET 4.5.1 pod Windows 7 x64 -- liczby oznaczają iteracje, które może uruchomić w 5 sekund, więc wyższe jest lepsze.

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 (tak to jeszcze 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 maksymalnie 14% (a większość kosztów jest spowodowana koniecznością uruchomienia GC, Uznaj to za najgorszy scenariusz realistycznie). 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);
}
 6
Author: Voo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-06-14 23:40:49

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

Zdemontowałem ciało pętli funkcji:

Testmetod1:

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

Testmetod2:

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, a co ważniejsze, wyjątek dorzucają ECMA spec :

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

WyjÄ…tki rzuca:

System.NullReferenceException

I

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

Exception it rzuty:

System.NullReferenceException
System.IndexOutOfRangeException
System.ArrayTypeMismatchException

Jak widzisz, stelem działa bardziej w sprawdzaniu zakresu tablic i sprawdzaniu typów. Ponieważ ciało pętli robi niewiele (przypisuje tylko wartość), narzut sprawdzania dominuje w czasie obliczeń. Dlatego wydajność różni się o 530%.

I to również odpowiada na twoje pytania: zagrożeniem jest brak sprawdzania zakresu i typu tablicy. Jest to niebezpieczne (jak wspomniano w deklaracji funkcji; D).

 5
Author: HKTonyLee,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-10-19 04:57:40

EDIT: (mała zmiana w kodzie i w pomiarach powoduje dużą zmianę w wyniku)

Najpierw uruchomiłem zoptymalizowany kod w debuggerze (F5), ale to było złe. Powinno być uruchamiane bez debuggera (Ctrl+F5). Po drugie, kod może być dokładnie zoptymalizowany, więc musimy go skomplikować, aby optymalizator nie zadzierał z naszymi pomiarami. Sprawiłem, że wszystkie metody zwracają ostatnią pozycję w tablicy, a tablica jest wypełniona inaczej. Ponadto istnieje dodatkowe zero W op TestMethod2, które zawsze sprawia, że dziesięć razy wolniej.

Próbowałem innych metod, oprócz tych dwóch, które podałeś. Metoda 3 ma ten sam kod co Metoda 2, ale funkcja jest zadeklarowana unsafe. Metoda 4 wykorzystuje dostęp wskaźnika do regularnie tworzonej tablicy. Metoda 5 wykorzystuje dostęp wskaźnika do niezarządzanej pamięci, jak opisał Marc Gravell. wszystkie pięć metod działa w bardzo podobnym czasie. M5 jest najszybszy (a M1 jest blisko sekundy). Różnica między najszybszym i najwolniejszym wynosi około 5%, co jest nie obchodzi mnie to.
    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
Author: Dialecticus,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-06-13 12:19:57