2015-11-05 10 views
14

Napisałem miękką aplikację czasu rzeczywistego w Haskell, która zajmuje się symulowaną fizyką, wykrywaniem kolizji, wszystkimi dobrymi rzeczami. Robiąc wszystko, przeznaczam dużo pamięci i prawdopodobnie mógłbym zoptymalizować wykorzystanie pamięci, gdybym chciał, ale skoro siedzę wygodnie na 40% procesorze i tylko 1% pamięci RAM i tak było używane, to nie wydaje się to konieczne. Widzę jednak, że dużo czasu, gdy rzuca śmieci, klatki są pomijane. Sprawdziłem, że to jest przyczyna problemu poprzez profilowanie z threadscope: nie ma użytecznych obliczeń, czasami nawet do 0,05 sekundy, gdy śmieciarz wykonuje swoją działalność, co skutkuje trzema pominiętymi ramkami, co jest bardzo zauważalne i bardzo irytujące .Jak zoptymalizować usuwanie pamięci dla miękkiej aplikacji czasu rzeczywistego w Haskell?

Próbowałem teraz rozwiązać ten problem, ręcznie wywołując performMinorGC każdą klatkę, i wydaje się to łagodzić problem, czyniąc go znacznie bardziej płynnym, z wyjątkiem faktu, że całkowite użycie procesora idzie drastycznie do około 70%. Najwyraźniej wolałbym tego uniknąć.

Kolejną rzeczą, której próbowałem, było zmniejszenie przestrzeni alokacji GC do 64k z 512k przy pomocy -H64k, a także próbowałem ustawić -I0.03, aby spróbować go częściej zbierać. Obie te opcje zmieniły schemat usuwania śmieci, który widziałem w threadscope, ale nadal powodowały pomijanie klatek.

Czy ktoś, kto ma trochę doświadczenia z optymalizacją GC, może mi tutaj pomóc? Czy jestem skazany na ręczne wywoływanie performMinorGC i rezygnację z ogromnej utraty wydajności?

EDIT

Próbowałem uruchomić go za podobną ilość czasu w tych testach, ale ponieważ jest to w czasie rzeczywistym, nie ma sensu, w którym to „Gotowe”.

statystyki środowiska z performMinorGC co 4 klatek:

 9,776,109,768 bytes allocated in the heap 
    349,349,800 bytes copied during GC 
     53,547,152 bytes maximum residency (14 sample(s)) 
     12,123,104 bytes maximum slop 
      105 MB total memory in use (0 MB lost due to fragmentation) 

            Tot time (elapsed) Avg pause Max pause 
    Gen 0  15536 colls, 15536 par 3.033s 0.997s  0.0001s 0.0192s 
    Gen 1  14 colls, 13 par 0.207s 0.128s  0.0092s 0.0305s 

    Parallel GC work balance: 6.15% (serial 0%, perfect 100%) 

    TASKS: 20 (2 bound, 13 peak workers (18 total), using -N4) 

    SPARKS: 74772 (20785 converted, 0 overflowed, 0 dud, 38422 GC'd, 15565 fizzled) 

    INIT time 0.000s ( 0.001s elapsed) 
    MUT  time 9.773s ( 7.368s elapsed) 
    GC  time 3.240s ( 1.126s elapsed) 
    EXIT time 0.003s ( 0.004s elapsed) 
    Total time 13.040s ( 8.499s elapsed) 

    Alloc rate 1,000,283,400 bytes per MUT second 

    Productivity 75.2% of total user, 115.3% of total elapsed 

gc_alloc_block_sync: 29843 
whitehole_spin: 0 
gen[0].sync: 11 
gen[1].sync: 71 

Bez performMinorGC

12,316,488,144 bytes allocated in the heap 
    447,495,936 bytes copied during GC 
     63,556,272 bytes maximum residency (15 sample(s)) 
     15,418,296 bytes maximum slop 
      146 MB total memory in use (0 MB lost due to fragmentation) 

            Tot time (elapsed) Avg pause Max pause 
    Gen 0  19292 colls, 19292 par 2.613s 0.950s  0.0000s 0.0161s 
    Gen 1  15 colls, 14 par 0.237s 0.165s  0.0110s 0.0499s 

    Parallel GC work balance: 2.67% (serial 0%, perfect 100%) 

    TASKS: 17 (2 bound, 13 peak workers (15 total), using -N4) 

    SPARKS: 100714 (29688 converted, 0 overflowed, 0 dud, 47577 GC'd, 23449 fizzled) 

    INIT time 0.000s ( 0.001s elapsed) 
    MUT  time 13.377s ( 9.917s elapsed) 
    GC  time 2.850s ( 1.115s elapsed) 
    EXIT time 0.000s ( 0.006s elapsed) 
    Total time 16.247s (11.039s elapsed) 

    Alloc rate 920,744,995 bytes per MUT second 

    Productivity 82.5% of total user, 121.4% of total elapsed 

gc_alloc_block_sync: 68533 
whitehole_spin: 0 
gen[0].sync: 9 
gen[1].sync: 147 

Całkowita wydajność wydaje się być niższa bez performMinorGC teraz, niż gdy testowałem go wczoraj z jakiegoś powodu - - zanim było zawsze> 90%.

+1

Proszę wkleić statystyki środowiska wykonawczego ('+ RTS -s') – Yuras

+1

naiwna sugestia, ale co jeśli po prostu wywołasz' performMinorGC' co, powiedzmy, 10 klatek? –

+1

Co przydzielasz? Jeśli możesz uniknąć przydziałów, GC staje się nie problem. – MathematicalOrchid

Odpowiedz

4

Masz różne duże stare pokolenie. Ma wielkość 100 MB.

Domyślnie GHC wykonuje większy GC, gdy wielkość sterty osiąga 2 x jego rozmiar po ostatnim większym GC. Oznacza to, że w pewnym momencie GC musiał przeskanować i skopiować 50Mb danych. Jeśli twój procesor ma limit przepustowości pamięci wynoszący 10 Gb, to ładowanie i kopiowanie 50Mb zajmie co najmniej 0,01 s (porównaj ze średnią dla gen1 i maks. Pauzą).

(Zakładam, że sprawdziłeś dziennik zdarzeń, aby upewnić się, że główny GC faktycznie działa podczas 0,05 s pauzy, więc nie ma problemu z synchronizacją nici, gdy GC czeka na inne wątki, zamiast wykonywać prawdziwą pracę.)

Aby zminimalizować przerwy w GC, należy zapewnić, że stara generacja jest mała. Jeśli większość z tych 50 MB jest statycznymi danymi przydzielonymi na samym początku i żyją do końca (np. Tekstury lub siatki), to jesteś stuck. Jedynym sposobem obejścia tego problemu jest spakowanie danych do np. zapamiętywany wektor i rozpakuj jego części ponownie, kiedy tego potrzebujesz.

Jeśli dane są przydzielane podczas wykonywania i mają ograniczony czas (ale wystarczający do przetrwania kilku głównych generacji), spróbuj ponownie przemyśleć plan. Zwykle żadne dane nie powinny przetrwać jednej klatki, więc robisz coś złego. Na przykład. zachowujesz dane, gdy nie powinieneś.

Inny zły znak - gen0 maks. Pauza 0.02sec. To dość dziwne. Domyślnie obszar alokacji gen0 wynosi 0,5Mb, więc gen0 GC powinien być szybki. Prawdopodobnie masz duży remembered set. Możliwa przyczyna: zmienne struktury (IORef, mutable Vector itp.) Lub wiele leniwych aktualizacji thunk.

Problem pomniejszy (prawdopodobnie niepowiązany): wygląda na to, że używasz niejawnego paralelizmu, ale tylko 1/3 iskier jest konwertowana. Przydzielasz zbyt wiele spartów, 1/2 z nich to GC.

+0

OK, w oparciu o to zrobiłem kilka eksperymentów i zakończyłem komentowanie WSZYSTKIEGO mojego logicznego kodu poza spinowaniem głównej pętli. Okazuje się, że jeśli to zrobię, osiągnę do 3,1 GB/s stopy przydziału. Więc coś w sposobie, w jaki zaimplementowałem główną pętlę oznacza, że ​​mnóstwo pamięci jest przydzielane na każdą iterację ... Chyba nie powinienem się dziwić, że GC powoduje tak duże przerwy, kiedy ma tak wiele do opowiedzenia! Sądzę, że musi to mieć coś wspólnego z moim stosem transformatorów. Spróbuję wyizolować ten problem. Przyjmuję odpowiedź, ponieważ powiedziała mi, co jest nie tak. –

+0

Aktualizacja: przyklejenie 'threadDelay 100' do głównej pętli znacznie złagodziło problem. Okazuje się, że ponieważ robię renderowanie w oddzielnym wątku, główna pętla obracała się w kółko bez żadnych rzeczywistych prac na większości iteracji. "ThreadDelay" wydaje się jednak trochę hack ... –