Почему этот код потребляет так много кучи?
Вот полный репозиторий. Это очень простой тест, который вставляет 50000 случайных вещей в базу данных с помощью простой привязки базы данных postgresql. Он использует MonadRandom и может генерировать вещи лениво.
Вот case1 и конкретный фрагмент кода с использованием генератора Thing:
insertThings c = do
ts <- genThings
withTransaction c $ do
executeMany c "insert into things (a, b, c) values (?, ?, ?)" $ map (\(Thing ta tb tc) -> (ta, tb, tc)) $ take 50000 ts
Вот case2, который просто выводит вещи в стандартный вывод:
main = do
ts <- genThings
mapM print $ take 50000 ts
В первом случае у меня очень плохие времена GC:
cabal-dev/bin/posttest +RTS -s
1,750,661,104 bytes allocated in the heap
619,896,664 bytes copied during GC
92,560,976 bytes maximum residency (10 sample(s))
990,512 bytes maximum slop
239 MB total memory in use (0 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 3323 colls, 0 par 11.01s 11.46s 0.0034s 0.0076s
Gen 1 10 colls, 0 par 0.74s 0.77s 0.0769s 0.2920s
INIT time 0.00s ( 0.00s elapsed)
MUT time 2.97s ( 3.86s elapsed)
GC time 11.75s ( 12.23s elapsed)
RP time 0.00s ( 0.00s elapsed)
PROF time 0.00s ( 0.00s elapsed)
EXIT time 0.00s ( 0.00s elapsed)
Total time 14.72s ( 16.09s elapsed)
%GC time 79.8% (76.0% elapsed)
Alloc rate 588,550,530 bytes per MUT second
Productivity 20.2% of total user, 18.5% of total elapsed
Хотя во втором случае времена замечательные
cabal-dev/bin/dumptest +RTS -s > out
1,492,068,768 bytes allocated in the heap
7,941,456 bytes copied during GC
2,054,008 bytes maximum residency (3 sample(s))
70,656 bytes maximum slop
6 MB total memory in use (0 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 2888 colls, 0 par 0.13s 0.16s 0.0001s 0.0089s
Gen 1 3 colls, 0 par 0.01s 0.01s 0.0020s 0.0043s
INIT time 0.00s ( 0.00s elapsed)
MUT time 2.00s ( 2.37s elapsed)
GC time 0.14s ( 0.16s elapsed)
RP time 0.00s ( 0.00s elapsed)
PROF time 0.00s ( 0.00s elapsed)
EXIT time 0.00s ( 0.00s elapsed)
Total time 2.14s ( 2.53s elapsed)
%GC time 6.5% (6.4% elapsed)
Alloc rate 744,750,084 bytes per MUT second
Productivity 93.5% of total user, 79.0% of total elapsed
Я пытался применить профилирование кучи, но ничего не понял. Похоже, что все 50000 Thing сначала создаются в памяти, затем преобразуются в ByteStrings с запросами, а затем эти строки отправляются в базу данных. Но почему это происходит? Как определить виновный код?
GHC версия 7.4.2
Флаги компиляции - -O2 для всех библиотек и самого пакета (скомпилированы cabal-dev в песочнице)
1 ответ
Я проверил профиль с FormatMany и 50k вещей. Память неуклонно растет, а затем быстро падает. Максимальный объем используемой памяти составляет чуть более 40 МБ. Основными центрами затрат являются buildQuery и escapeStringConn, за которыми следует toRow. Половина данных - это ARR_WORDS (байтовые строки), Действия и списки.
formatMany
в значительной степени делает один длинный ByteString
из частей, собранных из вложенных списков действий. Действия конвертируются в ByteString
Строители, которые сохраняют ByteStrings
пока не используется для производства окончательного долго строгого ByteString
, Эти ByteStrings живут долго до тех пор, пока не будет построена финальная BS.
Строки должны быть экранированы с помощью libPQ, поэтому любое не простое действие BS
передается в libPQ и заменяется новым в escapeStringConn и друзей, добавляя больше мусора. Если вы замените Text in Thing другим Int, время GC упадет с 75% до 45%.
Я попытался уменьшить использование временных списков с помощью formatMany и buildQuery, заменив mapM на foldM вместо Builder. Это мало помогает, но немного увеличивает сложность кода.
TLDR - Builders
нельзя употреблять лениво, потому что все они необходимы для производства окончательного строгого ByteString
(в значительной степени массив байтов). Если у вас есть проблемы с памятью, разделите executeMany на части внутри одной транзакции.