Нитки Haskell купують переповнення, незважаючи на загальне використання пам'яті лише 22 Мб?


114

Я намагаюся паралелізувати промінь-прослідковувач. Це означає, що у мене дуже довгий список невеликих обчислень. Програма ванілі працює на конкретній сцені за 67,98 секунд і 13 Мб загального використання пам'яті та 99,2% продуктивності.

У своїй першій спробі я застосував паралельну стратегію parBufferз розміром буфера 50. Я вибрав, parBufferтому що він проходить по списку так само швидко, як споживаються іскри, і не змушує хребта списку, як parList, що використовує багато пам'яті оскільки список дуже довгий. З -N2цим він працював за час 100,46 секунди та 14 Мб загального використання пам’яті та 97,8% продуктивності. Інформація про іскру:SPARKS: 480000 (476469 converted, 0 overflowed, 0 dud, 161 GC'd, 3370 fizzled)

Велика частка неясних іскор вказує на те, що зернистість іскор була занадто малою, тому далі я спробував використовувати стратегію parListChunk, яка розбиває список на шматки і створює іскру для кожного шматка. Я отримав найкращі результати з розміром шматка 0.25 * imageWidth. Програма працювала за 93,43 секунди та 236 Мб загального використання пам'яті та 97,3% продуктивності. Інформація іскра: SPARKS: 2400 (2400 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled). Я вважаю, що набагато більше використання пам'яті відбувається тому, що parListChunkзмушує хребет списку.

Тоді я спробував написати власну стратегію, яка ліниво розділила список на шматки, а потім передала шматки parBufferі зв'язала результати.

 concat $ withStrategy (parBuffer 40 rdeepseq) (chunksOf 100 (map colorPixel pixels))

Це зайняло 95,99 секунд і 22 Мб загального використання пам’яті та 98,8% продуктивності. Це було успішним в тому сенсі, що всі іскри перетворюються і використання пам'яті значно нижче, проте швидкість не покращується. Ось зображення частини профілю eventlog.Профіль журналу подій

Як видно, нитки зупиняються через переливи купи. Я спробував додати, +RTS -M1Gщо збільшує розмір купи за замовчуванням до 1 Гбіт. Результати не змінилися. Я читав, що головний потік Haskell використовуватиме пам'ять з купи, якщо його стек переповнюється, тому я також спробував збільшити розмір стека за замовчуванням, +RTS -M1G -K1Gале це також не вплинуло.

Чи можна ще щось спробувати? Я можу розмістити більш детальну інформацію про профілювання для використання пам’яті чи журналу подій, якщо потрібно, я не включив її всю, тому що це багато інформації, і я не вважав, що все це потрібно включати.

EDIT: Я читав про багатоядерну підтримку Haskell RTS , і вона говорить про те, що для кожного ядра є HEC (Haskell Execution Context). Кожен HEC містить, між іншим, зону розподілу (яка є частиною однієї спільної купи). Щоразу, коли будь-яка територія розподілу HEC вичерпана, слід проводити збирання сміття. Здається, це параметр RTS для управління ним, -A. Я спробував -A32M, але не побачив різниці.

EDIT2: Ось посилання на репортаж github, присвячений цьому питанню . Я включив результати профілювання в папку профілювання.

EDIT3: Ось відповідний біт коду:

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)))

Сітки - це випадкові поплавці, які попередньо обчислюються та використовуються colorPixel. Тип colorPixel:

 colorPixel :: World -> ((Float,Float),([(Float,Float)],[(Float,Float)])) -> Color

2
Не могли б ви вказати точну фіксацію, де ви намагалися concat $ withStrategy …? Я не можу відтворити таку поведінку 6008010, що є найближчим посиланням на вашу редакцію.
Зета

3
Я зробив виділений репо, щоб я випадково не заплутався в ньому. Я також включив всю інформацію про профілювання.
Джастін Реймонд

@dfeuer, коли я сказав, що я визначаю власну стратегію, я не мав на увазі Strategy. Треба було підібрати краще слово. Також проблема переповнення купи виникає з parListChunkі parBufferнадто.
Джастін Реймонд

Відповіді:


2

Не рішення вашої проблеми, а натяк на причину:

Хаскелл здається дуже консервативним у повторному використанні пам'яті, і коли інтерпретатор бачить потенціал для повернення блоку пам'яті, він йде на це. Опис вашої проблеми відповідає незначній поведінці GC, описаній тут (внизу) https://wiki.haskell.org/GHC/Memory_Management .

Нові дані розміщені в 512kb "розпліднику". Після того, як він вичерпаний, виникає "незначний GC" - він сканує дитячу і звільняє невикористані значення.

Тож якщо ви подрібнюєте дані на менші шматки, ви дозволяєте двигуну робити очищення раніше - GC запускається.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.