Próbuję dodać równoległość do programu, który konwertuje .bmp na skalę szarości .bmp. Widzę zazwyczaj 2-4x gorszą wydajność dla kodu równoległego. Zmieniam rozmiary parabufferów/chunkingów i nadal nie mogę o tym myśleć. Szukasz wskazówek.Lazy IO + Parallelism: konwertowanie obrazu na skalę szarości
Cały plik źródłowy użyty tutaj: http://lpaste.net/106832
Używamy Codec.BMP
czytać w strumieniu pikseli reprezentowana przez type RGBA = (Word8, Word8, Word8, Word8)
. Aby przekonwertować na skalę szarości, po prostu zamapuj transformację "luma" na wszystkie piksele.
Realizacja seryjny jest dosłownie:
toGray :: [RGBA] -> [RGBA]
toGray x = map luma x
Test .bmp wejście jest 5184 x 3456 (71,7 MB).
Implementacja szeregowa działa w ~ 10 s, ~ 550ns/piksel. Threadscope wygląda czysty:
Dlaczego jest to tak szybko? Przypuszczam, że ma coś z leniwym ByteStringiem (nawet jeśli Codec.BMP używa ścisłego ByteStringa - czy jest tu niejawna konwersja?) I fusion.
Dodawanie Równo- ległości
pierwsza próba była poprzez dodanie równoległość parList
. O chłopie. Program wykorzystał pamięć ~ 4-5 GB i system zaczął się wymieniać.
Potem przeczytałem rozdział "Paralizing Lazy Streams with parBuffer" w książce Simona Marlowa O'Reilly i wypróbowałem parBuffer
z dużym rozmiarem. To nadal nie dawało pożądanej wydajności. Rozmiary iskier były niewiarygodnie małe.
Potem próbowali zwiększyć rozmiar zapłonową przez wyrwy listę leniwy, a następnie trzyma się parBuffer
dla równoległości:
toGrayPar :: [RGBA] -> [RGBA]
toGrayPar x = concat $ (withStrategy (parBuffer 500 rpar) . map (map luma))
(chunk 8000 x)
chunk :: Int -> [a] -> [[a]]
chunk n [] = []
chunk n xs = as : chunk n bs where
(as,bs) = splitAt (fromIntegral n) xs
Ale to wciąż nie daje pożądanych wyników:
18,934,235,760 bytes allocated in the heap
15,274,565,976 bytes copied during GC
639,588,840 bytes maximum residency (27 sample(s))
238,163,792 bytes maximum slop
1910 MB total memory in use (0 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 35277 colls, 35277 par 19.62s 14.75s 0.0004s 0.0234s
Gen 1 27 colls, 26 par 13.47s 7.40s 0.2741s 0.5764s
Parallel GC work balance: 30.76% (serial 0%, perfect 100%)
TASKS: 6 (1 bound, 5 peak workers (5 total), using -N2)
SPARKS: 4480 (2240 converted, 0 overflowed, 0 dud, 2 GC'd, 2238 fizzled)
INIT time 0.00s ( 0.01s elapsed)
MUT time 14.31s (14.75s elapsed)
GC time 33.09s (22.15s elapsed)
EXIT time 0.01s ( 0.12s elapsed)
Total time 47.41s (37.02s elapsed)
Alloc rate 1,323,504,434 bytes per MUT second
Productivity 30.2% of total user, 38.7% of total elapsed
gc_alloc_block_sync: 7433188
whitehole_spin: 0
gen[0].sync: 0
gen[1].sync: 1017408
Jak mogę lepiej zrozumieć, co się tutaj dzieje?
Ustaliłeś rozsądny czas bazowy? Ile czasu zajmuje obliczenie długości '[RGBA]'? Ponieważ inne twoje komentarze wskazują, że ta wartość jest przesyłana strumieniowo z leniwym IO, jest całkiem możliwe, że czas IO zawsze będzie dominował przy przetwarzaniu twoich działań, równolegle lub nie. Więc ile czasu pracy to po prostu IO i parsowanie? – Carl
Mogę spróbować zobaczyć, jak długo trwa przetwarzanie IO i Codec.BMP. Linia bazowa, której używam, to seryjna implementacja, która trwa ~ 10 sekund. Sądzę, że jest to wystarczająco użyteczne, by porównać z 30-40, które zajmuje równoległa implementacja. – brooksbp