Próbuję zrównoleglenie ray-tracera. Oznacza to, że mam bardzo długą listę małych obliczeń. Program podstawowy działa na określonej scenie w 67,98 sekund i 13 MB całkowitego wykorzystania pamięci oraz 99,2% wydajności.
W mojej pierwszej próbie użyłem strategii równoległej parBuffer
z rozmiarem bufora 50. Wybrałem, parBuffer
ponieważ przegląda listę tylko tak szybko, jak zużywane są iskry, i nie wymusza na jej grzbiecie parList
, co wymagałoby dużo pamięci ponieważ lista jest bardzo długa. Z -N2
tym działał w czasie 100,46 sekundy i 14 MB całkowitego wykorzystania pamięci oraz 97,8% wydajności. Informacje o iskrze to:SPARKS: 480000 (476469 converted, 0 overflowed, 0 dud, 161 GC'd, 3370 fizzled)
Duża część iskier syczących wskazuje, że ziarnistość iskier była zbyt mała, więc następnie spróbowałem użyć strategii parListChunk
, która dzieli listę na kawałki i tworzy iskry dla każdego fragmentu. Najlepsze wyniki uzyskałem przy wielkości porcji 0.25 * imageWidth
. Program działał w 93,43 sekundy przy 236 MB całkowitego wykorzystania pamięci i 97,3% wydajności. Informacje iskra jest: SPARKS: 2400 (2400 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)
. Uważam, że dużo większe użycie pamięci wynika z tego, że parListChunk
wymusza kręgosłup listy.
Potem spróbowałem napisać własną strategię, która leniwie dzieliła listę na części, a następnie przekazywała je do parBuffer
i konkatenowała wyniki.
concat $ withStrategy (parBuffer 40 rdeepseq) (chunksOf 100 (map colorPixel pixels))
Trwało to 95,99 sekund i łącznie zajmowało 22 MB pamięci oraz 98,8% wydajności. Udało się to w tym sensie, że wszystkie iskry są konwertowane, a zużycie pamięci jest znacznie niższe, jednak prędkość nie ulega poprawie. Oto obraz części profilu eventlog.
Jak widać, wątki są zatrzymywane z powodu przepełnienia sterty. Próbowałem dodać, +RTS -M1G
co zwiększa domyślny rozmiar sterty aż do 1 GB. Wyniki się nie zmieniły. Czytałem, że główny wątek Haskell będzie używał pamięci ze sterty, jeśli jego stos się przepełni, więc próbowałem również zwiększyć domyślny rozmiar stosu, +RTS -M1G -K1G
ale to również nie miało wpływu.
Czy jest coś jeszcze, co mogę spróbować? W razie potrzeby mogę zamieścić bardziej szczegółowe informacje o profilowaniu wykorzystania pamięci lub dzienniku zdarzeń, nie uwzględniłem tego wszystkiego, ponieważ jest to dużo informacji i nie sądziłem, że wszystkie muszą być uwzględnione.
EDYCJA: Czytałem o obsłudze wielu rdzeni Haskell RTS i mówi o istnieniu HEC (Haskell Execution Context) dla każdego rdzenia. Każdy HEC zawiera, między innymi, obszar alokacji (który jest częścią jednego wspólnego stosu). Ilekroć którykolwiek obszar alokacji HEC jest wyczerpany, należy przeprowadzić czyszczenie pamięci. Wygląda na to, że jest to opcja RTS do sterowania nią, -A. Próbowałem -A32M, ale nie widziałem różnicy.
EDIT2: Oto link do repozytorium github poświęconego temu pytaniu . Wyniki profilowania umieściłem w folderze profilowania.
EDIT3: Oto odpowiedni bit kodu:
render :: [([(Float,Float)],[(Float,Float)])] -> World -> [Color]
render grids world = cs where
ps = [ (i,j) | j <- reverse [0..wImgHt world - 1] , i <- [0..wImgWd world - 1] ]
cs = map (colorPixel world) (zip ps grids)
--cs = withStrategy (parListChunk (round (wImgWd world)) rdeepseq) (map (colorPixel world) (zip ps grids))
--cs = withStrategy (parBuffer 16 rdeepseq) (map (colorPixel world) (zip ps grids))
--cs = concat $ withStrategy (parBuffer 40 rdeepseq) (chunksOf 100 (map (colorPixel world) (zip ps grids)))
Siatki są losowe pływaki, które są wykorzystywane przez precomputed i typu colorPixel.The z colorPixel
IS:
colorPixel :: World -> ((Float,Float),([(Float,Float)],[(Float,Float)])) -> Color
źródło
concat $ withStrategy …
? Nie mogę odtworzyć tego zachowania w programie6008010
, który jest najbliższym zatwierdzeniem Twojej zmiany.Strategy
. Powinienem był wybrać lepsze słowo. Ponadto występuje problem przepełnienia sterty w programieparListChunk
iparBuffer
.Odpowiedzi:
Nie rozwiązanie twojego problemu, ale wskazówka co do przyczyny:
Haskell wydaje się być bardzo konserwatywny w ponownym wykorzystaniu pamięci, a kiedy tłumacz widzi możliwość odzyskania bloku pamięci, robi to. Opis Twojego problemu pasuje do pomniejszego zachowania GC opisanego tutaj (na dole) https://wiki.haskell.org/GHC/Memory_Management .
Więc jeśli podzielisz dane na mniejsze fragmenty, włączysz silnik do wcześniejszego czyszczenia - włącza się GC.
źródło