Почему этот код потребляет так много кучи?

Вот полный репозиторий. Это очень простой тест, который вставляет 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 на части внутри одной транзакции.

Другие вопросы по тегам