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;
}
}
źródło
Marshal.AllocHGlobal
(nie zapomnijFreeHGlobal
też) przydzielić danych poza pamięć zarządzaną? Następnie rzuć wskaźnik na afloat*
i powinieneś zostać posortowany.Odpowiedzi:
Porównując kod testowy z Samem, stwierdziłem, że oboje mamy rację!
Jednak o różnych rzeczach:
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:
( 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:
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 :
Wygląda jednak na to, że stos jest coraz wolniejszy .
źródło
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ć
stackalloc
sł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.
źródło
float[]
dofloat*
. 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.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:
źródło
stackalloc
nie podlega śmieciu.stackalloc
- to musi trochę przeskoczyć i masz nadzieję, że zrobi to bez wysiłku - ale chcę, aby to wprowadziło niepotrzebne komplikacje / obawy. IMOstackalloc
ś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ć stosuJedną 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.
źródło
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:
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:
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.
źródło
stackalloc
, w którym przypadku mówimyfloat*
itp. - który nie ma takich samych kontroli. Nazywa się tounsafe
z bardzo dobrego powodu. Osobiście cieszę się, że mogę z niego skorzystać,unsafe
gdy jest ku temu dobry powód, ale Sokrates podaje kilka rozsądnych argumentów.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:
x86 JIT (tak, to wciąż trochę smutne):
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:
źródło
12500000
jako 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ę.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:
TestMethod2:
Możemy sprawdzić użycie instrukcji i, co ważniejsze, wyjątek, który wprowadzają w specyfikacji ECMA :
Wyjątki, które rzuca:
I
Wyjątek stanowi:
Jak widać,
stelem
wię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).
źródło
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,
TestMethod2
któ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.źródło
TestMethod4
vsTestMethod1
to znacznie lepsze porównanie dlastackalloc
.