Понимание смысла из GHC profiler

#haskell #profiling #ghc

#haskell #профилирование #ghc

Вопрос:

Я пытаюсь понять смысл из GHC profiler. Существует довольно простое приложение, которое использует werq lens-aeson библиотеки и, и, изучая профилирование GHC, я решил немного поиграть с ним.

Использование различных опций ( time инструмент, RTS -p -RTS и RTS -p -h ) Я приобрел совершенно другие показатели использования моей памяти. Имея все эти цифры, я теперь совершенно запутался, пытаясь понять, что происходит, и сколько памяти на самом деле использует приложение.

Эта ситуация напоминает мне фразу Артура Блоха: «Человек с часами знает, который час. Человек с двумя часами никогда не бывает уверен.

Не могли бы вы, пожалуйста, подсказать мне, как я могу прочитать все эти цифры и каково значение каждой из них.

Вот цифры:

time -l отчеты около 19 миллионов

 #/usr/bin/time -l ./simple-wreq
...
        3.02 real         0.39 user         0.17 sys
  19070976  maximum resident set size
         0  average shared memory size
         0  average unshared data size
         0  average unshared stack size
     21040  page reclaims
         0  page faults
         0  swaps
         0  block input operations
         0  block output operations
        71  messages sent
        71  messages received
      2991  signals received
        43  voluntary context switches
      6490  involuntary context switches
 

Использование RTS -p -RTS флаговых отчетов составляет около 92 млн. Хотя там написано «total alloc», мне кажется странным, что такое простое приложение, как это, может выделить и выпустить 91 млн.

 # ./simple-wreq  RTS -p -RTS      
# cat simple-wreq.prof
        Fri Oct 14 15:08 2016 Time and Allocation Profiling Report  (Final)

           simple-wreq  RTS -N -p -RTS

        total time  =        0.07 secs   (69 ticks @ 1000 us, 1 processor)
        total alloc =  91,905,888 bytes  (excludes profiling overheads)

COST CENTRE                             MODULE                          %time %alloc

main.g                                  Main                             60.9   88.8
MAIN                                    MAIN                             24.6    2.5
decodeLenient/look                      Data.ByteString.Base64.Internal   5.8    2.6
decodeLenientWithTable/fill             Data.ByteString.Base64.Internal   2.9    0.1
decodeLenientWithTable...fill         Data.ByteString.Base64.Internal   1.4    0.0
decodeLenientWithTable...fill.       Data.ByteString.Base64.Internal   1.4    0.1
decodeLenientWithTable...fill.... Data.ByteString.Base64.Internal   1.4    3.3
decodeLenient                           Data.ByteString.Base64.Lazy       1.4    1.4


                                                                                                             individual     inherited
COST CENTRE                                              MODULE                            no.     entries  %time %alloc   %time %alloc

MAIN                                                     MAIN                              443           0   24.6    2.5   100.0  100.0
 main                                                    Main                              887           0    0.0    0.0    75.4   97.4
  main.g                                                 Main                              889           0   60.9   88.8    75.4   97.4
   object_                                               Data.Aeson.Parser.Internal        925           0    0.0    0.0     0.0    0.2
    jstring_                                             Data.Aeson.Parser.Internal        927          50    0.0    0.2     0.0    0.2
   unstream/resize                                       Data.Text.Internal.Fusion         923         600    0.0    0.3     0.0    0.3
   decodeLenient                                         Data.ByteString.Base64.Lazy       891           0    1.4    1.4    14.5    8.1
    decodeLenient                                        Data.ByteString.Base64            897         500    0.0    0.0    13.0    6.7
....
 

RTS -p -h и hp2ps покажите мне следующую картинку и два числа: 114 КБ в заголовке и что-то около 1,8 Мб на графике.
Профилирование памяти

И, на всякий случай, вот приложение:

 module Main where

import Network.Wreq
import Control.Lens
import Data.Aeson.Lens
import Control.Monad

main :: IO ()
main = replicateM_ 10 g
  where 
    g = do
        r <- get "http://httpbin.org/get"
        print $ r ^. responseBody
                   . key "headers"
                   . key "User-Agent"
                   . _String
 

ОБНОВЛЕНИЕ 1: Спасибо всем за невероятно хорошие ответы. Как было предложено, я добавляю RTS -s выходные данные, так что вся картина создается для всех, кто ее читает.

 #./simple-wreq  RTS -s
...
     128,875,432 bytes allocated in the heap
      32,414,616 bytes copied during GC
       2,394,888 bytes maximum residency (16 sample(s))
         355,192 bytes maximum slop
               7 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0       194 colls,     0 par    0.018s   0.022s     0.0001s    0.0022s
  Gen  1        16 colls,     0 par    0.027s   0.031s     0.0019s    0.0042s
 

ОБНОВЛЕНИЕ 2: Размер исполняемого файла:

 #du -h simple-wreq
63M     simple-wreq
 

Ответ №1:

Человек с часами знает, который час. Человек с двумя часами никогда не уверен.

Ах, но что показывают два наблюдения? Оба предназначены для отображения текущего времени в UTC? Или один из них должен показывать время в UTC, а другой — время в определенной точке на Марсе? Пока они синхронизированы, второй сценарий не будет проблемой, не так ли?

И это именно то, что здесь происходит. Вы сравниваете разные измерения памяти:

  • максимальная резидентность
  • общий объем выделенной памяти

Максимальный объем памяти — это наибольший объем памяти, который ваша программа когда-либо использовала в данный момент времени. Это 19 МБАЙТ. Однако общий объем выделенной памяти намного больше, поскольку именно так работает GHC: он «выделяет» память для объектов, которые собирают мусор, то есть почти все, что не распаковано.

Давайте рассмотрим пример C для этого:

 int main() {
   int i;
   char * mem;

   for(i = 0; i < 5;   i) {
      mem = malloc(19 * 1000 * 1000);
      free(mem);
   }
   return 0;
}
 

Всякий раз, когда мы используем malloc , мы будем выделять 19 мегабайт памяти. Однако сразу после этого мы освобождаем память. Таким образом, самый большой объем памяти, который у нас когда-либо был, составляет 19 мегабайт (и немного больше для стека и самой программы).

Однако в общей сложности мы выделяем 5 * 19 млн, итого 95 млн. Тем не менее, мы могли бы нормально запустить нашу маленькую программу, имея всего 20 мегабайт оперативной памяти. В этом разница между общим объемом выделенной памяти и максимальным сроком хранения. Обратите внимание, что резидентность, указываемая временем, всегда как минимум du <executable> , поскольку она тоже должна храниться в памяти.

При этом самый простой способ сгенерировать статистику -s — это показать, каков был максимальный резидент с точки зрения программы Haskell. В вашем случае это будет 1,9M, число в вашем профиле кучи (или удвоенное количество из-за профилирования). И да, исполняемые файлы Haskell, как правило, становятся чрезвычайно большими, поскольку библиотеки статически связаны.

Комментарии:

1. Поблагодарите Зету за ваш ответ. У меня есть еще один вопрос. Размер двоичного файла составляет 63 метра. Означает ли это, что двоичный файл каким-то образом не был полностью загружен в память?

2. @Slabko: На данный момент у меня нет среды Unix для проверки семантики time -l , так что это может быть неверно на 100%. Однако я не нашел ни одной реальной справочной страницы, где -l это описано. Что time вы используете?

3. Я использую Mac OS 10.11 и встроенную версию time . Он использует POSIX getrusage . Интересно, getusage что возвращает размер в кб, а 19070976 кб — это действительно большое число. Я попробовал GNU time в Linux; при двоичном размере 48M он по-прежнему утверждает, что максимальный размер резидентного набора равен 32820 КБ, но я думаю, что я получил более чем хороший ответ на свой вопрос, и теперь я понимаю, как я должен читать номера GHC profiler. Я не думаю, что способ time работы имеет отношение к пониманию оптимизации памяти Haskell в моем случае. Большое вам спасибо за вашу помощь, я действительно ценю это!

4. Весь исполняемый файл не обязательно должен быть загружен во время выполнения. Если исполняемый файл имеет таблицу символов, она может быть больше, чем фактический код и данные.

Ответ №2:

time -l отображает (резидентный, т. Е. Не Заменяемый) Размер процесса, как это видно операционной системой (очевидно). Это включает в себя удвоенный максимальный размер кучи Haskell (из-за того, как работает GC GHC), плюс все остальное, выделенное RTS или другими библиотеками C, плюс код самого вашего исполняемого файла плюс библиотеки, от которых он зависит, и т.д. Я предполагаю, что в этом случае основным вкладчиком в 19M является размер вашего исполняемого файла.

total alloc это общая сумма, выделенная на кучу Haskell. Это вовсе не показатель максимального размера кучи (именно это люди обычно подразумевают под «сколько памяти использует моя программа»). Выделение очень дешево, а скорость выделения около 1 ГБ / с типична для программы на Haskell.

Число в заголовке вывода hp2ps «114 272 байта х секунд» — это снова нечто совершенно другое: оно является интегралом от графика и измеряется в байтах * секундах, а не в байтах. Например, если ваша программа удерживает структуру размером 10 МБ в течение 4 секунд, то это приведет к увеличению этого числа на 40 МБ * с.

Число около 1,8 МБ, показанное на графике, — это фактический максимальный размер кучи Haskell, который, вероятно, вас больше всего интересует.

Вы опустили самый полезный источник данных о выполнении вашей программы, с помощью которого она выполняется RTS -s (для этого даже не требуется, чтобы она была собрана с профилированием).