Spec-Zone.ru › Haskell 9

8. Профилирование

GHC поставляется с системой профилирования времени и памяти, позволяющей ответить на вопросы типа «почему моя программа работает так медленно?» или «почему моя программа потребляет так много памяти?». Начнём с описания профилирования времени.

Профилирование времени программы — это трёхэтапный процесс:

  1. Перекомпилируйте программу для профилирования с помощью опции -prof и, вероятно, одной из опций для добавления автоматических аннотаций: рекомендуемая опция — -fprof-late.
  2. После компиляции программы для профилирования, вам необходимо запустить её для генерации профиля. Например, простой профиль времени можно создать, запустив программу с помощью +RTS -p (см. -p), что сгенерирует файл с именем prog.prof, где ⟨prog⟩ — имя вашей программы (без расширения .exe , если вы работаете в Windows).

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

  3. Проанализируйте сгенерированную информацию о профилировании, используйте её для оптимизации программы и повторите при необходимости.

Профилировщик времени измеряет время ЦП, затрачиваемое кодом Haskell в вашем приложении. В частности, время, затраченное на безопасные внешние вызовы, не отслеживается профилировщиком (см. Профилирование и внешние вызовы).

8.1. Центры затрат и стеки центров затрат

Система профилирования GHC назначает затраты центрам затрат. Затраты — это просто время или память (в байтах), необходимые для вычисления выражения. Центры затрат — это програмные аннотации вокруг выражений; все затраты, понесенные аннотированным выражением, присваиваются окружающему центру затрат. Кроме того, GHC будет запоминать стек окружающих центров затрат для любого данного выражения во время выполнения и генерировать дерево вызовов с присвоением затрат.

Давайте рассмотрим пример:

main = print (fib 30)
fib n = if n < 2 then 1 else fib (n-1) + fib (n-2)

Компилируйте и запускайте эту программу следующим образом:

$ ghc -prof -fprof-auto -rtsopts Main.hs
$ ./Main +RTS -p
121393
$

Когда GHC-скомпилированная программа запускается с опцией RTS -p, она генерирует файл с именем prog.prof. В этом случае файл будет содержать что-то вроде этого:

        Wed Oct 12 16:14 2011 Time and Allocation Profiling Report  (Final)

           Main +RTS -p -RTS

        total time  =        0.68 secs   (34 ticks @ 20 ms)
        total alloc = 204,677,844 bytes  (excludes profiling overheads)

COST CENTRE MODULE  %time %alloc

fib         Main    100.0  100.0


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

MAIN        MAIN                    102           0    0.0    0.0   100.0  100.0
 CAF        GHC.IO.Handle.FD        128           0    0.0    0.0     0.0    0.0
 CAF        GHC.IO.Encoding.Iconv   120           0    0.0    0.0     0.0    0.0
 CAF        GHC.Conc.Signal         110           0    0.0    0.0     0.0    0.0
 CAF        Main                    108           0    0.0    0.0   100.0  100.0
  main      Main                    204           1    0.0    0.0   100.0  100.0
   fib      Main                    205     2692537  100.0  100.0   100.0  100.0

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

Вторая часть файла — это разбиение по центрам затрат наиболее дорогостоящих функций в программе. В этом случае в программе была только одна значимая функция, а именно fib, и она была ответственна за 100% как затрат времени, так и затрат выделения памяти программы.

Третья и последняя секция файла предоставляет разбиение по стеку центра затрат. Это примерно профиль дерева вызовов программы. В приведённом примере ясно, что дорогостоящий вызов fib пришёл из main.

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

Полезность стеков центров затрат лучше демонстрируется путём незначительного изменения примера:

main = print (f 30 + g 30)
  where
    f n  = fib n
    g n  = fib (n `div` 2)

fib n = if n < 2 then 1 else fib (n-1) + fib (n-2)

Скомпилируйте и запустите эту программу, как и прежде, и посмотрите на новые результаты профилирования:

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

MAIN        MAIN                    102           0    0.0    0.0   100.0  100.0
 CAF        GHC.IO.Handle.FD        128           0    0.0    0.0     0.0    0.0
 CAF        GHC.IO.Encoding.Iconv   120           0    0.0    0.0     0.0    0.0
 CAF        GHC.Conc.Signal         110           0    0.0    0.0     0.0    0.0
 CAF        Main                    108           0    0.0    0.0   100.0  100.0
  main      Main                    204           1    0.0    0.0   100.0  100.0
   main.g   Main                    207           1    0.0    0.0     0.0    0.1
    fib     Main                    208        1973    0.0    0.1     0.0    0.1
   main.f   Main                    205           1    0.0    0.0   100.0   99.9
    fib     Main                    206     2692537  100.0   99.9   100.0   99.9

Теперь, хотя у нас было два вызова fib в программе, сразу же становится ясно, что вызов из f потребовал всего времени. Функциям f и g, определённым в where секции в main, присваиваются свои собственные центры затрат main.f и main.g соответственно.

Фактическое значение различных столбцов в выводе:

Количество раз, когда эта конкретная точка в дереве вызовов была введена.

Процент общего времени выполнения программы, затраченного в этой точке дерева вызовов.

Процент от общего выделения памяти (за исключением накладных расходов профилирования) программы, выполненный этим вызовом.

Процент общего времени выполнения программы, затраченного ниже этой точки в дереве вызовов.

Процент общего выделения памяти (за исключением накладных расходов профилирования) программы, выполненный этим вызовом и всеми его подвызовами.

Кроме того, вы можете использовать опцию RTS -P, чтобы получить следующую дополнительную информацию:

ticks

Исходное количество «тиков» времени, которые были присвоены этому центру затрат; из этого мы получаем значение %time , упомянутое выше.

bytes

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

Что насчёт рекурсивных функций и взаимно рекурсивных групп функций? Где присваиваются затраты? Ну, хотя GHC и сохраняет информацию о том, какие группы функций рекурсивно вызывали друг друга, эта информация не отображается в базовом профиле времени и выделения памяти; вместо этого граф вызовов сглаживается в дерево следующим образом: вызов функции, который происходит где-то в текущем стеке, не добавляет ещё одну запись в стек; вместо этого затраты на этот вызов агрегируются в вызывающую функцию [2].

8.1.1. Вставка центров затрат вручную

Центры затрат — это просто аннотации программы. Когда вы говорите -fprof-auto компилятору, он автоматически вставляет аннотацию центра затрат вокруг каждой привязки, не помеченной INLINE в вашей программе, но вы полностью свободны добавлять аннотации центров затрат самостоятельно. Будьте осторожны, добавляя слишком много аннотаций центров затрат, так как оптимизатор старается не перемещать их или удалять, что может существенно повлиять на оптимизацию вашей программы, а следовательно, и на время выполнения!

Синтаксис аннотации центра затрат для выражений

{-# SCC "name" #-} <expression>

где "name" — произвольная строка, которая станет именем вашего центра затрат в выводе профилирования, а <expression> — любое выражение Haskell. Аннотация SCC распространяется как можно дальше вправо при разборе, имея тот же приоритет, что и лямбда-абстракции, выражения let и условные выражения. Кроме того, аннотация не может появляться в позиции, где она изменит группировку подвыражений:

a = 1 / 2 / 2                          -- accepted (a=0.25)
b = 1 / {-# SCC "name" #-} 2 / 2       -- rejected (instead of b=1.0)

Это ограничение необходимо для сохранения свойства, что вставка прагмы, подобно вставке комментария, не имеет непреднамеренных последствий для семантики программы, в соответствии с GHC Proposal #176.

SCC означает «Установить центр затрат». Двойные кавычки можно опустить, если name — идентификатор Haskell, начинающийся с маленькой буквы, например:

{-# SCC id #-} <expression>

Аннотации центров затрат также могут появляться на уровне верхнего уровня или в контексте объявления. В этом случае вам необходимо передать имя функции, определённой в том же модуле или области, что и аннотация. Пример:

f x y = ...
  where
    g z = ...
    {-# SCC g #-}

{-# SCC f #-}

Если вы хотите дать центру затрат другое имя, отличное от имени функции, вы можете передать строку в аннотацию

f x y = ...
{-# SCC f "cost_centre_name" #-}

Вот пример программы с парой SCC:

main :: IO ()
main = do let xs = [1..1000000]
          let ys = [1..2000000]
          print $ {-# SCC last_xs #-} last xs
          print $ {-# SCC last_init_xs #-} last (init xs)
          print $ {-# SCC last_ys #-} last ys
          print $ {-# SCC last_init_ys #-} last (init ys)

что даёт такой профиль при запуске:

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

MAIN            MAIN                    102           0    0.0    0.0   100.0  100.0
 CAF            GHC.IO.Handle.FD        130           0    0.0    0.0     0.0    0.0
 CAF            GHC.IO.Encoding.Iconv   122           0    0.0    0.0     0.0    0.0
 CAF            GHC.Conc.Signal         111           0    0.0    0.0     0.0    0.0
 CAF            Main                    108           0    0.0    0.0   100.0  100.0
  main          Main                    204           1    0.0    0.0   100.0  100.0
   last_init_ys Main                    210           1   25.0   27.4    25.0   27.4
   main.ys      Main                    209           1   25.0   39.2    25.0   39.2
   last_ys      Main                    208           1   12.5    0.0    12.5    0.0
   last_init_xs Main                    207           1   12.5   13.7    12.5   13.7
   main.xs      Main                    206           1   18.8   19.6    18.8   19.6
   last_xs      Main                    205           1    6.2    0.0     6.2    0.0

8.1.2. Правила присвоения затрат

Во время выполнения программы с включённым профилированием GHC поддерживает стек центра затрат за кулисами и присваивает любые затраты (выделение памяти и время) тому текущему стеку центра затрат, который находится в момент возникновения затрат.

Механизм прост: всякий раз, когда программа вычисляет выражение с аннотацией SCC, {-# SCC c -#} E, центр затрат c помещается в текущий стек, и счётчик входов для этого стека увеличивается на единицу. Стек также иногда должен быть сохранён и восстановлен; в частности, когда программа создаёт кэширование (ленивую приостановку), текущий стек центра затрат сохраняется в кэше, и восстанавливается, когда кэш оценивается. Таким образом, стек центра затрат независим от фактического порядка оценки, используемого GHC во время выполнения.

При вызове функции GHC берёт стек, сохранённый в вызываемой функции (который для функции верхнего уровня будет пустым), и присоединяет его к текущему стеку, игнорируя любой префикс, идентичный префиксу текущего стека.

Ранее мы упоминали, что ленивые вычисления, то есть кэширования, сохраняют текущий стек при их создании и восстанавливают этот стек при их оценке. А что насчёт кэширований верхнего уровня? Они «создаются» при компиляции программы, так какой стек мы должны им дать? Техническое название кэширования верхнего уровня — CAF («Постоянная прикладная форма»). GHC присваивает каждому CAF в модуле стек, состоящий из единственного центра затрат M.CAF, где M — имя модуля. Также можно дать каждому CAF другой стек, используя опцию -fprof-cafs. Это особенно полезно при компиляции с помощью -ffull-laziness (как по умолчанию с -O и выше), поскольку константы в телах функций будут подняты на верхний уровень и станут CAF. Вероятно, вам придётся обратиться к ядру (-ddump-simpl), чтобы определить, чему соответствуют эти CAF.

8.2. Профилирование и внешние вызовы

Проще говоря, профайлер учитывает время, затраченное на небезопасные внешние вызовы, но игнорирует время, затраченное на безопасные внешние вызовы. Например, время, затраченное на ожидание операций ввода-вывода (например, getLine) не учитывается в профиле, поскольку getLine реализуется с помощью безопасного внешнего вызова.

Профайлер оценивает время работы процессора только для потоков Haskell в программе. В частности, время, «затраченное» программой на ожидание безопасных внешних вызовов, не учитывается в профилях времени. В среде выполнения есть понятие виртуального процессора, который называется «возможностью». Потоки Haskell выполняются на возможностях, и профайлер отслеживает возможности, чтобы определить, что выполняется в определённое время. Когда выполняется безопасный внешний вызов, он выполняется вне контекста возможности; поэтому отслеживание не учитывает затраченное время. Пока выполняется безопасный вызов, другие потоки Haskell могут свободно выполняться на возможности, и их затраты будут приписаны профайлеру. По окончании безопасного вызова заблокированный, не запланированный поток может быть возобновлён и запланирован.

Однако время ожидания на небезопасных внешних вызовах учитывается в профиле. Это происходит потому, что небезопасные внешние вызовы выполняются той же возможностью, на которой выполняется вызывающий их поток Haskell. Таким образом, небезопасный внешний вызов заблокирует всю возможность во время выполнения, и всякий раз, когда возможность отслеживается, «стоимость» внешнего вызова будет приписана стеку вызывающего центра затрат.

Однако обратите внимание, что вы не должны использовать небезопасные внешние вызовы для операций, которые блокируют! Не стоит заменять безопасные внешние вызовы небезопасными только для того, чтобы они отображались в профиле. Это предотвращает выполнение сборки мусора до возвращения внешнего вызова, что может быть катастрофичным для производительности.

8.3. Параметры компилятора для профилирования

-prof

Для использования системы профилирования все модули должны быть скомпилированы и связаны с параметром -prof. Любые аннотации SCC в вашем исходном коде оживут.

Без параметра -prof, ваши SCC игнорируются; поэтому вы можете компилировать код, содержащий SCC без внесения изменений.

-fno-prof-count-entries

Указывает GHC не собирать информацию о том, как часто функции вызываются во время выполнения (столбец «входы» в профиле времени) для данного модуля. Это обычно ускоряет работу профилируемого кода и приближает его к скорости непрофилируемого кода, так как GHC может более агрессивно оптимизировать, если ему не нужно поддерживать правильные счётчики входов. Этот параметр может быть полезен, если вы не заинтересованы в счётчиках входов (например, если вы планируете только профилирование кучи).

Существует несколько других параметров компиляции, связанных с профилированием. Используйте их в дополнение к -prof. Их необязательно использовать для всех модулей программы.

8.3.1. Автоматическое размещение центров затрат

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

-fprof-callers=⟨name⟩

Автоматически заключает все случаи вызова указанной функции в SCC. Обратите внимание, что эти центры затрат добавляются на поздней стадии компиляции (после упрощения), и поэтому имена могут немного отличаться от имен в исходной программе (например, вызов f может быть вложен в его обертку, что приведет к появлению вызова его вспомогательной функции $wf).

В дополнение к простым модульным именам (например, GHC.Base.map) ⟨name⟩ также поддерживает небольшой язык подстановок, используя * в качестве символа подстановки:

pattern    := <module> '.' <identifier>
module     := '*'
            | <Haskell module name>
identifier := <ident_char>
ident

Например, следующие шаблоны являются допустимыми:

  • Data.List.map
  • *.map
  • *.parse*
  • *.<\*>

Символ * может быть использован буквально путём экранирования (например, \*).

-fprof-auto

Все связи, не помеченные INLINE, независимо от того, экспортируются они или нет, являются верхнего уровня или вложенными, будут снабжены автоматическими аннотациями SCC. Функции, помеченные INLINE, должны быть снабжены центром затрат вручную.

-fprof-auto-top

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

-fprof-auto-exported

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

-fprof-auto-calls

Добавляет автоматическую аннотацию SCC ко всем местам вызова. Это особенно полезно при использовании профилирования для генерации стэков вызовов; см. функцию Debug.Trace.traceShow или флаг RTS -xc (Параметры RTS для отладки программ Haskell) для получения дополнительной информации.

-fprof-late
Since:

9.4.1

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

Хотя результаты обычно информативны, некоторые внутренние имена компилятора могут просочиться в профиль. Кроме того, если функция встраивается в место использования, её затраты будут учтены в центре затрат вызывающей функции.

Например, если у нас есть такой код:

{-# INLINE mysum #-}
mysum = sum
main = print $ mysum [1..9999999]

Тогда mysum не будет отображаться в профиле, так как он будет вложен в main, а значит, связанные с ним затраты будут приписаны неявному центру затрат main.

-fprof-late-inline
Since:

9.4.1

Добавляет автоматическую аннотацию SCC ко всем связям верхнего уровня поздно на стадии компиляции, после работы оптимизатора. Это аналогично -fprof-late, за исключением того, что центры затрат включаются в некоторые развёртывания.

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

Вы можете попробовать этот режим, если -fprof-late приводит к слишком сложному для интерпретации профилю.

-fprof-late-overloaded
Since:

9.10.1

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

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

-fprof-late-overloaded-calls
Since:

9.10.1

Добавляет автоматическую аннотацию SCC ко всем местам вызова, включающим аргументы словаря, на поздней стадии компиляции после работы оптимизатора и создания развёртываний. Это означает, что эти центры затрат не будут мешать оптимизациям на уровне ядра, и полученный профиль будет ближе к профилю производительности оптимизированной не профилируемой программы.

Этот флаг потенциально более полезен, чем -fprof-late-overloaded, поскольку он также добавит аннотации SCC в места вызова импортированных перегруженных функций.

Некоторые перегруженные вызовы могут не быть аннотированы, особенно в случаях, когда оптимизатор превращает перегруженную функцию в точку соединения. Вызовы таких функций не будут обернуты в аннотации SCC, поскольку это сделает их нехвостовыми вызовами, что является требованием для точек соединения. Вместо этого аннотации SCC добавляются вокруг тела перегруженных переменных соединения и получают отличные имена (join-rhs-<var>) для избежания путаницы.

-fprof-cafs

Затраты всех CAFs в модуле обычно приписываются одному «большому» центру затрат CAF. С этой опцией все CAFs получают свой собственный центр затрат. Вариант «если все остальное не сработает»…

-fprof-manual
Default:

on

Обрабатывает (или игнорирует) аннотации центров затрат, указанные вручную. Может быть полезно игнорировать аннотации из библиотек, которые нежелательны.

-auto-all

Устаревшее алиас для -fprof-auto

-auto

Устаревшее алиас для -fprof-auto-exported

-caf-all

Устаревшее алиас для -fprof-cafs

-no-auto-all

Устаревшее алиас для -fno-prof-auto

-no-auto

Устаревшее алиас для -fno-prof-auto

-no-caf-all

Устаревшее алиас для -fno-prof-cafs

8.4. Профилирование времени и распределения памяти

Для генерации профиля времени и распределения памяти укажите одну из следующих опций RTS компилируемой программе при её запуске (опции RTS должны быть заключены в +RTS ... -RTS как обычно):

-p
-P
-pa

Опция -p генерирует стандартный отчёт профиля времени. Он записывается в файл <stem>.prof; по умолчанию в качестве основы используется имя программы, но это можно изменить с помощью флага -po ⟨stem⟩.

Опция -P генерирует более подробный отчёт, содержащий фактические данные о времени и распределении памяти. (Практически не используется.)

Опция -pa генерирует самый подробный отчёт, содержащий все центры затрат в дополнение к фактическим данным о времени и распределении памяти.

-pj

Опция -pj генерирует отчёт о профиле времени/распределения памяти в формате JSON, записывая его в файл <program>.prof.

-po ⟨stem⟩

Опция -po ⟨stem⟩ переопределяет основу, используемую для формирования путей к выходным файлам для профилировщика центров затрат (см. флаги -p и -pj выше) и профилировщика кучи (см. -h).

Например, запуск программы с +RTS -h -p -pohello-world создаст профиль кучи с именем hello-world.hp и профиль центров затрат с именем hello-world.prof.

-V ⟨secs⟩
По умолчанию:

0,001 при профилировании и 0,01 в противном случае

Устанавливает интервал, с которым тикает внутренний системный таймер RTS, что также является интервалом выборки профиля времени и распределения памяти. По умолчанию он равен 0,001 секунды при профилировании и 0,01 в противном случае. Система выполнения использует один сигнал таймера для подсчета тиков; этот сигнал таймера используется для управления таймером переключения контекста (Использование Concurrent Haskell) и таймером профилирования кучи Опции RTS для профилирования кучи. Также, профилировщик времени использует сигнал таймера RTS напрямую для записи образцов профилирования времени.

Обычно, установка опции -V ⟨secs⟩ напрямую не требуется: разрешение таймера RTS автоматически корректируется, если короткий интервал запрошен с помощью опций -C ⟨s⟩ или -i ⟨secs⟩. Однако, установка -V ⟨secs⟩ требуется для повышения разрешения профилировщика времени.

Использование значения ноль полностью отключает внутренний системный таймер RTS и имеет эффект отключения таймеров, которые зависят от него: таймер переключения контекста и таймер профилирования кучи. Переключения контекста всё ещё будут происходить, но детерминированно и со скоростью значительно большей, чем обычно. Отключение таймера интервалов полезно для отладки, так как это устраняет источник недетерминированности во время выполнения.

-xc

Эта опция заставляет систему выполнения выводить текущий стек центров затрат всякий раз, когда возникает исключение. Это может быть особенно полезно для отладки местоположения исключений, таких как печально известная ошибка Prelude.head: empty list. См. Опции RTS для покрытия Haskell-программы.

8.4.1. Формат профиля JSON

профили в читаемом машиной формате JSON. Файл JSON может быть загружен непосредственно в speedscope.app для интерактивного просмотра профиля.

Объект верхнего уровня этого формата имеет следующие свойства,

program (string)

Имя программы

arguments (list of strings)

Аргументы командной строки, переданные программе

rts_arguments (list of strings)

Аргументы командной строки, переданные системе выполнения

initial_capabilities (integral number)

Количество возможностей, с которыми была запущена программа (например, с помощью опции -N ⟨x⟩). Обратите внимание, что количество возможностей может меняться во время выполнения из-за функции setNumCapabilities.

total_time (number)

Общее время выполнения программы в секундах.

total_ticks (integral number)

Количество «тиков» профилировщика, прошедших за время выполнения программы.

end_time (number)

Приблизительное время завершения выполнения программы как временная метка эпохи Unix.

tick_interval (float)

Сколько времени между тиками профилировщика.

total_alloc (integer)

Накопленное распределение памяти программы в байтах.

cost_centres (list of objects)

Список центров затрат программы

profile (object)

Сам древовидный профиль

Каждая запись в cost_centres — это объект, описывающий центр затрат программы, имеющий следующие свойства,

id (integral number)

Уникальный идентификатор, используемый для ссылки на центр затрат

is_caf (boolean)

Является ли центр затрат постоянной прикладной формой (CAF)

label (string)

Дескриптивная строка, приблизительно определяющая центр затрат.

src_loc (string)

Строка, описывающая область исходного кода, охватывающую центр затрат.

Сами данные профиля описываются полем profile, которое содержит древовидный объект (который мы здесь назовём «стеком центра затрат») с такими свойствами,

id (integral number)

Значение id центра затрат, указанного в списке cost_centres.

entries (integral number)

Сколько раз был задействован этот центр затрат?

ticks (integral number)

Сколько тиков занял процесс выполнения программы внутри этого центра затрат? Это не включает дочерние центры затрат.

alloc (integral number)

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

children (list)

Список, содержащий стеки дочерних центров затрат.

Например, простой профиль может выглядеть так,

{
  "program": "Main",
  "arguments": [
    "nofib/shootout/n-body/Main",
    "50000"
  ],
  "rts_arguments": [
    "-pj",
    "-hy"
  ],
  "end_time": "Thu Feb 23 17:15 2017",
  "initial_capabilities": 0,
  "total_time": 1.7,
  "total_ticks": 1700,
  "tick_interval": 1000,
  "total_alloc": 3770785728,
  "cost_centres": [
    {
      "id": 168,
      "label": "IDLE",
      "module": "IDLE",
      "src_loc": "<built-in>",
      "is_caf": false
    },
    {
      "id": 156,
      "label": "CAF",
      "module": "GHC.Integer.Logarithms.Internals",
      "src_loc": "<entire-module>",
      "is_caf": true
    },
    {
      "id": 155,
      "label": "CAF",
      "module": "GHC.Integer.Logarithms",
      "src_loc": "<entire-module>",
      "is_caf": true
    },
    {
      "id": 154,
      "label": "CAF",
      "module": "GHC.Event.Array",
      "src_loc": "<entire-module>",
      "is_caf": true
    }
  ],
  "profile": {
    "id": 162,
    "entries": 0,
    "alloc": 688,
    "ticks": 0,
    "children": [
      {
        "id": 1,
        "entries": 0,
        "alloc": 208,
        "ticks": 0,
        "children": [
          {
            "id": 22,
            "entries": 1,
            "alloc": 80,
            "ticks": 0,
            "children": []
          }
        ]
      },
      {
        "id": 42,
        "entries": 1,
        "alloc": 1632,
        "ticks": 0,
        "children": []
      }
    ]
  }
}

8.4.2. Формат профиля журнала событий

В дополнение к форматам .prof и .json определения центров затрат и образцы также выводятся в журнал событий eventlog. Формат событий указан в разделе кодировки журналов событий.

8.5. Профилирование использования памяти

Помимо профилирования времени и поведения выделения памяти вашей программы, вы также можете сгенерировать график ее использования памяти во времени. Это полезно для выявления причин утечек памяти, когда ваша программа удерживает больше памяти во время выполнения, чем ей необходимо. Утечки памяти приводят к замедлению выполнения из-за интенсивной работы сборщика мусора и могут даже привести к тому, что программа вообще выйдет из памяти.

Профилирование кучи отличается от временного профилирования тем, что не всегда необходимо использовать среду выполнения профилирования для генерации профиля кучи. Существуют два режима профилирования кучи (-hT и -hi [1]), которые всегда доступны.

Для генерации профиля кучи вашей программы:

  1. Предполагая, что вам нужна среда выполнения профилирования, скомпилируйте программу для профилирования (Параметры компилятора для профилирования).
  2. Запустите ее с одним из вариантов профилирования кучи, описанных ниже (например, -hc для базового профиля производителя) и включите журнал событий с помощью -l.

    Образцы кучи будут выводиться в журнал событий GHC (см. Вывод журнала событий профилировщика кучи для получения подробной информации о формате событий).

  3. Отобразите профиль кучи с помощью eventlog2html. Это создает файл HTML, который содержит визуализированный профиль.
  4. Откройте рендеренный интерактивный профиль в вашем веб-браузере.

Например, вот профиль кучи, созданный с использованием профилирования журнала событий в GHC, компилирующий библиотеку Cabal. Вы можете прочитать гораздо больше о eventlog2html на сайте.

_images/eventlog_profile.png

Обратите внимание, что существует устаревший prog.hp формат, который был отменён в пользу профилирования на основе журнала событий. Для того, чтобы отобразить устаревший формат, выполните следующие шаги.

  1. Запустите hp2ps для создания файла PostScript, prog.ps. Утилита hp2ps подробно описана в hp2ps — рендеринг профилей кучи в PostScript.
  2. Отобразите профиль кучи с помощью программы просмотра PostScript, такой как Ghostview, или распечатайте его на принтере, поддерживающем PostScript.

Например, вот профиль кучи, созданный для программы sphere из набора бенчмарков GHC nofib,

_images/prof_scc.svg

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

8.5.1. Параметры RTS для профилирования кучи

Можно сгенерировать несколько разных профилей кучи. Все типы профилей отображают график живой кучи по времени, но они различаются тем, как живая куча разбивается на группы. Следующие параметры RTS выбирают используемую разбивку:

-hT

Разбивает график по типу закрытия кучи. Это не требует времени выполнения профилирования.

-hc

Требует -prof. Разбивает график по стеку центра затрат, который произвел данные.

-hm

Требует -prof. Разбивает живую кучу по модулю, содержащему код, который произвел данные.

-hd

Требует -prof. Разбивает график по описанию замыкания. Для фактических данных описанием является только имя конструктора, для других замыканий это строка, сгенерированная компилятором, идентифицирующая замыкание.

-hy

Требует -prof. Разбивает график по типу. Для замыканий, которые имеют тип функции или неизвестный/полиморфный тип, строка будет представлять приближение к фактическому типу.

-he
Since:

9.10.1

Требует -prof. Разбивает график по эре.

Каждое замыкание помечено эрой, в которой оно создано. Эры начинаются с 1 и могут быть установлены в вашей программе для областей значений с помощью функций из GHC.Profiling.Eras или автоматически увеличиваться временем выполнения с помощью --automatic-era-increment.

-hr

Требует -prof. Разбивает график по набору держателей. Подробнее о профилировании держателей см. ниже (Профилирование держателей).

-hb

Требует -prof. Разбивает график по биографии. Подробнее о биографическом профилировании см. ниже (Биографическое профилирование).

-hi

Разбивает график по адресу таблицы сведений замыкания. Для получения полезного результата программа должна быть скомпилирована с -finfo-table-map, но это не требует времени выполнения профилирования.

-l

Выводить образцы профилей в журнал событий GHC. Этот формат более выразителен, чем старый .hp формат, и его можно сопоставить с другими событиями в течение времени выполнения программы. Подробности о структуре созданных событий см. в разделе Вывод журнала событий профилировщика кучи.

Кроме того, профиль может быть ограничен данными кучи, которые удовлетворяют определенным критериям. Например, вы можете отобразить профиль по типу, но только для данных, полученных определенным модулем, или профиль по держателю для определенного типа данных. Ограничения задаются следующим образом:

-hc ⟨name⟩

Требует -prof. Ограничить профиль замыканиями, созданными стеками центров затрат, в которых один из указанных центров затрат находится вверху.

-hC ⟨name⟩

Требует -prof. Ограничить профиль замыканиями, созданными стеками центров затрат, в которых один из указанных центров затрат находится где-то в стеке.

-hm ⟨module⟩

Требует -prof. Ограничить профиль замыканиями, созданными указанными модулями.

-hd ⟨desc⟩

Требует -prof. Ограничить профиль замыканиями с указанными строками описания.

-hy ⟨type⟩

Требует -prof. Ограничить профиль замыканиями с указанными типами.

-he ⟨era⟩

Требует -prof. Ограничить профиль указанной эрой.

-hr ⟨cc⟩

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

-hb ⟨bio⟩

Требует -prof. Ограничить профиль замыканиями с одной из указанных биографий, где ⟨bio⟩ — это один из lag, drag, void, или use.

Например, следующие параметры сгенерируют профиль держателей, ограниченный Branch и Leaf конструкторами:

prog +RTS -hr -hdBranch,Leaf

Может быть только один параметр «разбивки» (например, -hr в приведенном выше примере), но нет ограничений на количество дополнительных ограничений. Все параметры могут быть объединены, за исключением одного: GHC в настоящее время не поддерживает смешивание -hr и -hb параметров.

Есть еще три параметра, относящихся к профилированию кучи:

-i ⟨secs⟩

Установить интервал профилирования (выборки) на ⟨secs⟩ секунд (по умолчанию 0,1 секунды). Разрешены дроби: например -i0.2 будет получать 5 выборок в секунду. Это относится только к профилированию кучи; временные профили всегда выбираются с частотой системных часов RTS. См. Временное и профилирование выделения памяти для изменения этого.

--no-automatic-heap-samples
Since:

9.2.1

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

--no-automatic-time-samples
Since:

9.10.1

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

--automatic-era-increment
Since:

9.10.1

Увеличивать эру на 1 при каждом основном сборе мусора. Используется совместно с -he.

--null-eventlog-writer
Since:

9.2.2

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

-L ⟨num⟩

Устанавливает максимальную длину имени стека центра затрат в профиле кучи. По умолчанию 25.

8.5.2. Профилирование удерживающих объектов

Профилирование удерживающих объектов предназначено для ответов на вопросы типа «почему эти данные удерживаются?». Начнём с определения удерживающего объекта:

Удерживающим объектом может быть системный стек, неузнанный замыкание (thunk), или явным образом изменяемый объект.

В частности, конструкторы не являются удерживающими объектами.

Объект B удерживает объект A если (i) B является удерживающим объектом и (ii) объект A может быть достигнут путём рекурсивного следования по указателям, начиная с объекта B, но без встреч других удерживающих объектов по пути. Каждый живой объект удерживается одним или несколькими удерживающими объектами, в совокупности называемыми его набором удерживающих объектов, или его удерживающими объектами.

При запросе профилирования удерживающих объектов с помощью опции -hr, генерируется граф, разбитый по наборам удерживающих объектов. Набор удерживающих объектов отображается как набор стеков центров затрат; поскольку это обычно слишком большой объём для отображения на графике профиля, каждый набор удерживающих объектов нумеруется и отображается на графике в сокращённом виде вместе с его номером, а полный список наборов удерживающих объектов выгружается в файл prog.prof.

Профилирование удерживающих объектов требует нескольких проходов по живой куче, чтобы обнаружить полный набор удерживающих объектов для каждого объекта, что может быть довольно медленным. Поэтому мы устанавливаем ограничение на максимальный размер набора удерживающих объектов, где все наборы удерживающих объектов, превышающие максимальный размер набора удерживающих объектов, заменяются специальным набором MANY. Максимальный размер набора по умолчанию равен 8 и может быть изменён с помощью опции RTS -R ⟨size⟩:

-R ⟨size⟩

Ограничить количество элементов в наборе удерживающих объектов до ⟨размер⟩ (по умолчанию 8).

8.5.2.1. Рекомендации по использованию профилирования удерживающих объектов

Определение удерживающих объектов разработано для отражения распространённой причины утечек памяти: большая структура удерживается неузнанным вычислением и будет освобождена после принудительного выполнения вычисления. Хороший пример — поиск значения в конечном отображении, где, если поиск не будет принудительно выполнен в своевременном порядке, неузнанный поиск приведёт к удержанию всего отображения. Эти типы утечек памяти часто можно устранить, принудительно выполняя соответствующие вычисления жёстко, используя seq или аннотации строгости на полях конструкторов данных.

Часто определённая структура данных удерживается цепочкой неузнанных замыканий, только самое близкое из которых будет сообщено профилированием удерживающих объектов — например, A удерживает B, B удерживает C, и C удерживает большую структуру. Может быть большое количество Bs, но только одно A, поэтому A — это то, что нас действительно интересует. Однако, в этом случае профилирование удерживающих объектов сообщит, что B является удерживающим объектом большой структуры. Чтобы переместиться выше по цепочке удерживающих объектов, мы можем запросить другой профиль удерживающих объектов, но на этот раз ограничить профиль объектами B, чтобы получить профиль удерживающих объектов B:

prog +RTS -hr -hcB

Этот трюк не является безошибочным, потому что в куче могут быть другие B замыкания, которые не являются искомыми удерживающими объектами, но мы обнаружили, что это полезный метод в большинстве случаев.

8.5.3. Точный анализ удерживающих объектов

Если вы хотите точно ответить на вопросы о том, почему определённый тип замыкания удерживается, то стоит воспользоваться ghc-debug, который имеет интерфейс терминала, позволяющий легко отвечать на такие запросы, как: что удерживает определённое замыкание.

8.5.4. Профилирование биографии

Типичный объект кучи может находиться в одном из четырёх состояний в любой момент своего жизненного цикла:

  • Этап отставания, который является промежутком между созданием и первым использованием объекта,
  • этап использования, который длится от первого использования до последнего использования объекта, и
  • Этап оттягивания, который длится от последнего использования до момента падения последней ссылки на объект.
  • Объект, который никогда не используется, находится в состоянии «пустоты» в течение всего своего жизненного цикла.

Профиль биографии кучи отображает часть живой кучи в каждом из четырёх перечисленных состояний. Обычно наиболее интересными состояниями являются состояния «пустоты» и «оттягивания»: живая куча в этих состояниях скорее всего представляет собой потерянные ресурсы, чем куча в состояниях «отставания» или «использования».

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

prog +RTS -hc -hbdrag,void

После того, как вы узнаете производителя или тип кучи в состояниях «оттягивания» или «пустоты», следующим шагом обычно является поиск удерживающего(их) объекта(ов):

prog +RTS -hr -hccc...

Примечание

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

8.5.5. Фактическая память

Как резиденция кучи, отчитываемая профилировщиком кучи, связана с фактической резиденцией памяти вашей программы при её запуске? Вы можете увидеть существенную разницу между резиденцией, отчитываемой профилировщиком кучи, и резиденцией, отчитываемой инструментами вашей системы (например, ps или top в Unix или диспетчере задач в Windows). Есть несколько причин для этого:

  • Существует накладные расходы на само профилирование, которые вычитаются из показателей резиденции профилировщиком. Эта накладная часть исчезает при компиляции без поддержки профилирования, разумеется. Текущая накладная часть составляет 2 дополнительных слова на объект кучи, что, вероятно, приводит к примерно 30% накладных расходов.
  • Для выполнения сборки мусора требуется больше памяти, чем фактическая резиденция. Коэффициент зависит от алгоритма сборки мусора: основная сборка мусора в стандартном копирующем коллекторе поколений обычно потребует \(3L\) байт памяти, где \(L\) — объём живых данных. Это потому, что по умолчанию (см. опцию RTS -F ⟨factor⟩) мы разрешаем старому поколению увеличиться вдвое (\(2L\)) перед его сбором, и нам дополнительно требуется \(L\) байт для копирования живых данных. При использовании компактной сборки (см. опцию -c) это сокращается до \(2L\), и может быть дополнительно уменьшено путём изменения опции -F ⟨factor⟩. Также добавьте размер области выделения (см. -A ⟨size⟩).
  • Текст самой программы, С-стек, любые данные вне кучи (например, данные, выделенные сторонними библиотеками, и данные, выделенные RTS), и память mmap() не учитываются в профиле кучи.

Для более подробного обсуждения понимания того, как резиденция памяти процесса соответствует живым данным, см. Понимание того, как использование памяти ОС соответствует живым данным.

8.6. hp2ps – Визуализация профилей кучи в PostScript

Использование:

hp2ps [flags] [<file>[.hp]]

Программа hp2ps преобразует файл .hp, созданный с помощью опции выполнения -h<break-down>, в график кучи в PostScript. По соглашению, файл, который обрабатывается hp2ps, имеет расширение .hp. Вывод в PostScript записывается в file@.ps. Если <file> опущена, программа работает как фильтр.

Программа hp2ps распространяется в ghc/utils/hp2ps в дистрибутиве исходного кода GHC. Она была первоначально разработана Дейвом Уэйклином как часть профилировщика кучи HBC/LML.

Флаги:

-d

Для повышения читаемости графиков, hp2ps сортирует затенённые полосы для каждого идентификатора. По умолчанию полосы с наибольшей площадью накладываются друг на друга. Флаг -d приводит к тому, что более грубые полосы (представляющие серии значений с наибольшими стандартными отклонениями) накладываются на более гладкие.

-b

Обычно hp2ps помещает заголовок графика в небольшой прямоугольник в верхней части страницы. Однако, если строка JOB слишком длинная для маленького прямоугольника (более 35 символов), то hp2ps использует большой прямоугольник. Флаг -b заставляет hp2ps использовать большой прямоугольник.

-e⟨float⟩[in|mm|pt]

Генерирует инкапсулированный PostScript, подходящий для включения в документы LaTeX. Обычно график PostScript рисуется в альбомной ориентации в области шириной 9 дюймов и высотой 6 дюймов, и hp2ps размещает эту область примерно по центру листа формата A4. Этот формат удобен для детального изучения графика, но не подходит для включения в документы LaTeX. Флаг -e вызывает вывод графика в книжной ориентации, где float указывает ширину в дюймах, миллиметрах или пунктах (по умолчанию). Полученный файл PostScript соответствует стандарту Encapsulated PostScript (EPS) и может быть включён в документ LaTeX с помощью конвертера dvi-в-PostScript от Рокицки dvips.

-g

Создаёт вывод, подходящий для gs просмотрщика PostScript (или аналогичного). В этом случае график печатается в книжной ориентации без масштабирования. Вывод не подходит для лазерного принтера.

-l

Обычно профиль ограничивается 20 полосами, а дополнительные идентификаторы группируются в полосу OTHER. Флаг -l удаляет это ограничение в 20 полос, создавая необходимое количество полос. Ключ не генерируется, так как он не поместится! Это полезно для профилей времени создания с множеством полос.

-m⟨int⟩

Обычно профиль ограничивается 20 полосами, а дополнительные идентификаторы группируются в полосу OTHER. Флаг -m задаёт альтернативное ограничение на количество полос (максимум 20).

-m0 удаляет ограничение на количество полос. Создаётся необходимое количество полос. Ключ не генерируется, так как он не поместится! Это полезно для отображения профилей времени создания с множеством полос.

-p

Использовать предыдущие параметры. По умолчанию график PostScript автоматически масштабируется по горизонтали и вертикали, чтобы заполнить страницу. Однако при подготовке серии графиков для презентации часто бывает полезно нарисовать новый график, используя тот же масштаб, затенение и порядок, что и предыдущий. Флаг -p вызывает рисование графика с параметрами, определёнными предыдущим запуском hp2ps на file. Они извлекаются из file@.aux.

-s

Использовать небольшой прямоугольник для заголовка.

-t⟨float⟩

Обычно элементы следа, сумма которых составляет менее 1% от профиля, удаляются из профиля. Флаг -t позволяет изменить этот процент (максимум 5%).

-t0 запрашивает не удалять элементы следа из профиля, обеспечивая отображение всех данных.

-c

Генерировать цветной вывод.

-y

Игнорировать метки.

-?

Вывести информацию об использовании.

8.7. Профилирование параллельных и конкурирующих программ

Сочетание -threaded и -prof вполне допустимо, и, действительно, можно профилировать программу, работающую на нескольких процессорах, с помощью опции RTS -N ⟨x⟩.

[3]

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

Мы настоятельно рекомендуем использовать -fno-prof-count-entries при компиляции программы для профилирования на нескольких ядрах, поскольку счётчики вхождений также хранятся в общей памяти, а их постоянное обновление на нескольких ядрах чрезвычайно медленное.

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

8.8. Наблюдение за покрытием кода

Инструменты покрытия кода позволяют программисту определить, какие части кода были фактически выполнены, а какие никогда не вызывались. GHC имеет опцию для генерации инструментированного кода, который записывает покрытие кода в качестве части набора инструментов Haskell Program Coverage (HPC), включенного в GHC. Инструменты HPC могут использоваться для отображения сгенерированной информации о покрытии кода в понятном для человека формате.

Правильно инструментированный код предоставляет информацию о покрытии двух типов: покрытие исходного кода и покрытие логических условий. Покрытие исходного кода — это степень использования каждой части программы, измеряемая на трех разных уровнях: объявления (как верхнего уровня, так и локальные), альтернативы (среди нескольких уравнений или ветвей case) и выражения (на каждом уровне). Покрытие логических условий — это степень, в которой каждое из значений True и False получено в каждом синтаксическом контексте булевого выражения (т. е. в условии, условии if, квалификаторе).

HPC отображает оба вида информации двумя основными способами: текстовые отчеты со статистическими данными (hpc report) и исходный код с цветовой разметкой (Recip.hs). Для логического покрытия существует четыре возможных результата для каждого условия if, условия или квалификатора: встречаются как True, так и False; только True; только False; никогда не вычислялось. В выходных данных hpc-markup выделение жёлтым фоном указывает на часть программы, которая никогда не была вычислена; зелёный фон указывает на выражение, которое всегда равно True, а красный — на выражение, которое всегда равно False.

8.8.1. Маленький пример: Вычисление обратных величин

В качестве примера представлена программа, называемая Recip.hs, которая вычисляет точные десятичные представления обратных величин, при этом повторяющиеся части показаны в квадратных скобках.

reciprocal :: Int -> (String, Int)
reciprocal n | n > 1 = ('0' : '.' : digits, recur)
             | otherwise = error
              "attempting to compute reciprocal of number <= 1"
  where
  (digits, recur) = divide n 1 []
divide :: Int -> Int -> [Int] -> (String, Int)
divide n c cs | c `elem` cs = ([], position c cs)
              | r == 0      = (show q, 0)
              | r /= 0      = (show q ++ digits, recur)
  where
  (q, r) = (c*10) `quotRem` n
  (digits, recur) = divide n r (c:cs)

position :: Int -> [Int] -> Int
position n (x:xs) | n==x      = 1
                  | otherwise = 1 + position n xs

showRecip :: Int -> String
showRecip n =
  "1/" ++ show n ++ " = " ++
  if r==0 then d else take p d ++ "(" ++ drop p d ++ ")"
  where
  p = length d - r
  (d, r) = reciprocal n

main = do
  number <- readLn
  putStrLn (showRecip number)
  main

Инструментация HPC включается с флагом -fhpc:

$ ghc -fhpc Recip.hs

GHC создает подкаталог .hpc в текущей директории и помещает в него файлы индекса HPC (.mix) по одному на каждый скомпилированный модуль. Вам не нужно беспокоиться об этих файлах: они содержат информацию, необходимую инструменту hpc для генерации данных о покрытии для скомпилированных модулей после выполнения программы.

$ ./Recip
1/3
= 0.(3)

Запуск программы создаёт файл с суффиксом .tix, в данном случае Recip.tix, который содержит данные о покрытии для данного запуска программы. Программа может быть запущена несколько раз (например, с различными тестовыми данными), и данные о покрытии из отдельных запусков накапливаются в файле .tix. Это поведение можно контролировать с помощью --read-tix-file=<yes|no>. Вы можете контролировать местоположение файла .tix с помощью переменной среды HPCTIXFILE.

HPCTIXFILE

Устанавливает путь к выводу файла HPC .tix.

После запуска программы мы можем сгенерировать текстовый свод покрытия:

$ hpc report Recip
 80% expressions used (81/101)
 12% boolean coverage (1/8)
      14% guards (1/7), 3 always True,
                        1 always False,
                        2 unevaluated
       0% 'if' conditions (0/1), 1 always False
     100% qualifiers (0/0)
 55% alternatives used (5/9)
100% local declarations used (9/9)
100% top-level declarations used (5/5)

Мы также можем сгенерировать помеченный исходный код.

$ hpc markup Recip
writing Recip.hs.html

Это создает один файл на каждый модуль Haskell и 4 файла индекса, hpc_index.html, hpc_index_alt.html, hpc_index_exp.html, hpc_index_fun.html.

8.8.2. Параметры для инструментирования кода для покрытия

-fhpc

Включить покрытие кода для текущего модуля или модулей, которые компилируются.

Модули, скомпилированные с этой опцией, могут быть свободно смешаны с модулями, скомпилированными без неё; фактически, большинство библиотек обычно компилируются без -fhpc. Когда программа запускается, данные о покрытии генерируются только для тех модулей, которые были скомпилированы с -fhpc, и инструмент hpc будет отображать информацию только об этих модулях.

-hpcdir⟨dir⟩
По умолчанию:

.hpc

Переопределить директорию, в которую GHC помещает файлы индекса HPC (.mix) используемые hpc для понимания структуры программы.

8.8.3. Набор инструментов hpc

Команда hpc имеет несколько подкоманд:

$ hpc
Usage: hpc COMMAND ...

Commands:
  help        Display help for hpc or a single command
Reporting Coverage:
  report      Output textual report about program coverage
  markup      Markup Haskell source with program coverage
Processing Coverage files:
  sum         Sum multiple .tix files in a single .tix file
  combine     Combine two .tix files in a single .tix file
  map         Map a function over a single .tix file
Coverage Overlays:
  overlay     Generate a .tix file from an overlay file
  draft       Generate draft overlay that provides 100% coverage
Others:
  show        Show .tix file in readable, verbose format
  version     Display version for hpc

В целом, эти параметры действуют на файл .tix после того, как инструментированный двоичный файл его сгенерировал.

Инструмент hpc предполагает, что вы находитесь в корневой директории расположения вашего приложения, а файл .tix находится в той же корневой директории. Вы можете использовать флаг --srcdir для использования hpc для любой другой директории, и использовать --srcdir несколько раз для анализа программ, скомпилированных из различных расположений, как это типично для пакетов.

Теперь мы более подробно объясним основные режимы работы hpc.

8.8.3.1. hpc report

hpc report предоставляет текстовый отчёт о покрытии. По умолчанию все модули и пакеты учитываются при генерации отчёта, если не используются опции include или exclude. Отчёт является сводным, если не используется флаг --per-module. Опция --xml-output позволяет инструментам использовать hpc для получения данных о покрытии.

$ hpc help report
Usage: hpc report [OPTION] .. <TIX_FILE> [<MODULE> [<MODULE> ..]]

Options:

    --per-module                  show module level detail
    --decl-list                   show unused decls
    --exclude=[PACKAGE:][MODULE]  exclude MODULE and/or PACKAGE
    --include=[PACKAGE:][MODULE]  include MODULE and/or PACKAGE
    --srcdir=DIR                  path to source directory of .hs files
                                  multi-use of srcdir possible
    --hpcdir=DIR                  append sub-directory that contains .mix files
                                  default .hpc [rarely used]
    --reset-hpcdirs               empty the list of hpcdir's
                                  [rarely used]
    --xml-output                  show output in XML

8.8.3.2. hpc markup

hpc markup помечает исходные файлы цветными метками html.

$ hpc help markup
Usage: hpc markup [OPTION] .. <TIX_FILE> [<MODULE> [<MODULE> ..]]

Options:

    --exclude=[PACKAGE:][MODULE]  exclude MODULE and/or PACKAGE
    --include=[PACKAGE:][MODULE]  include MODULE and/or PACKAGE
    --srcdir=DIR                  path to source directory of .hs files
                                  multi-use of srcdir possible
    --hpcdir=DIR                  append sub-directory that contains .mix files
                                  default .hpc [rarely used]
    --reset-hpcdirs               empty the list of hpcdir's
                                  [rarely used]
    --fun-entry-count             show top-level function entry counts
    --highlight-covered           highlight covered code, rather that code gaps
    --destdir=DIR                 path to write output to

8.8.3.3. hpc sum

hpc sum складывает любое количество файлов .tix в один файл .tix. hpc sum не изменяет исходный файл .tix; он генерирует новый файл .tix.

$ hpc help sum
Usage: hpc sum [OPTION] .. <TIX_FILE> [<TIX_FILE> [<TIX_FILE> ..]]
Sum multiple .tix files in a single .tix file

Options:

    --exclude=[PACKAGE:][MODULE]  exclude MODULE and/or PACKAGE
    --include=[PACKAGE:][MODULE]  include MODULE and/or PACKAGE
    --output=FILE                 output FILE
    --union                       use the union of the module namespace (default is intersection)

8.8.3.4. hpc combine

hpc combine - это универсальный инструмент для hpc. Он может использоваться для вычисления разницы между файлами .tix, для вычитания одного файла .tix из другого или для сложения двух файлов .tix. hpc combine не изменяет исходный файл .tix; он генерирует новый файл .tix.

$ hpc help combine
Usage: hpc combine [OPTION] .. <TIX_FILE> <TIX_FILE>
Combine two .tix files in a single .tix file

Options:

    --exclude=[PACKAGE:][MODULE]  exclude MODULE and/or PACKAGE
    --include=[PACKAGE:][MODULE]  include MODULE and/or PACKAGE
    --output=FILE                 output FILE
    --function=FUNCTION           combine .tix files with join function, default = ADD
                                  FUNCTION = ADD | DIFF | SUB
    --union                       use the union of the module namespace (default is intersection)

8.8.3.5. hpc map

hpc map инвертирует или обнуляет файл .tix. hpc map не изменяет исходный файл .tix; он генерирует новый файл .tix.

$ hpc help map
Usage: hpc map [OPTION] .. <TIX_FILE>
Map a function over a single .tix file

Options:

    --exclude=[PACKAGE:][MODULE]  exclude MODULE and/or PACKAGE
    --include=[PACKAGE:][MODULE]  include MODULE and/or PACKAGE
    --output=FILE                 output FILE
    --function=FUNCTION           apply function to .tix files, default = ID
                                  FUNCTION = ID | INV | ZERO
    --union                       use the union of the module namespace (default is intersection)

8.8.3.6. hpc overlay и hpc draft

Наложения (overlays) — это экспериментальная функция HPC, текстовое описание покрытия. hpc draft используется для генерации черновика наложения из файла .tix, а hpc overlay генерирует файл .tix из наложения.

% hpc help overlay
Usage: hpc overlay [OPTION] .. <OVERLAY_FILE> [<OVERLAY_FILE> [...]]

Options:

    --srcdir=DIR   path to source directory of .hs files
                   multi-use of srcdir possible
    --hpcdir=DIR                  append sub-directory that contains .mix files
                                  default .hpc [rarely used]
    --reset-hpcdirs               empty the list of hpcdir's
                                  [rarely used]
    --output=FILE  output FILE
% hpc help draft
Usage: hpc draft [OPTION] .. <TIX_FILE>

Options:

    --exclude=[PACKAGE:][MODULE]  exclude MODULE and/or PACKAGE
    --include=[PACKAGE:][MODULE]  include MODULE and/or PACKAGE
    --srcdir=DIR                  path to source directory of .hs files
                                  multi-use of srcdir possible
    --hpcdir=DIR                  append sub-directory that contains .mix files
                                  default .hpc [rarely used]
    --reset-hpcdirs               empty the list of hpcdir's
                                  [rarely used]
    --output=FILE                 output FILE

8.8.4. Ограничения и недостатки покрытия кода Haskell

HPC не пытается заблокировать файл .tix, поэтому несколько одновременно выполняемых двоичных файлов в одной директории будут демонстрировать состояние гонки. Во время компиляции нет возможности изменить имя сгенерированного файла .tix; во время выполнения имя сгенерированного файла .tix можно изменить с помощью HPCTIXFILE; имя файла .tix также изменится, если вы переименуете исполняемый файл. HPC не работает с GHCi.

8.9. Использование профилирования «ticky-ticky» (для разработчиков)

-ticky

Включить профилирование ticky-ticky. По умолчанию оно отслеживает только выделения по каждому типу замыкания. Смотрите -ticky-allocd, чтобы отслеживать также выделения из каждого типа замыкания.

Профилировщик ticky-ticky GHC предоставляет средство низкого уровня для отслеживания количества входов и выделений конкретных отдельных замыканий. Профилирование ticky-ticky требует определенной осведомлённости с внутренним устройством GHC, поэтому оно лучше всего подходит для опытных пользователей, но может предоставить неоценимый точный взгляд на поведение выделения памяти в ваших программах.

Начало работы с профилированием ticky состоит из трёх шагов.

  1. Добавьте флаг -ticky при компиляции модуля Haskell, чтобы включить профилирование «ticky-ticky» для этого модуля. Это заставляет GHC генерировать инструкции подсчёта производительности в каждой функции STG.
  2. Добавьте -ticky в командную строку при компоновке, чтобы скомпоновать с версией системы выполнения, позволяющей отображать результаты. Фактически, в фазе компоновки -ticky подразумевает -debug, поэтому вы получаете и отладочную версию системы выполнения.
  3. Затем при запуске программы вы можете собрать результаты профилирования двумя способами.
  • Используя журнал событий, флаг -lT будет периодически отправлять в журнал событий образцы ticky. Преимущество этого в возможности определения динамического поведения в течение всего жизненного цикла программы. Смотрите Счётчики Ticky для получения подробной информации о типах событий, которые сообщаются. Информация ticky может быть представлена в интерактивной таблице с помощью eventlog2html.
  • Используется устаревший текстовый формат с помощью флага -r ⟨file⟩. Это создаёт текстовую таблицу, содержащую информацию о том, сколько раз каждый счётчик активировался за время выполнения программы.

8.9.1. Дополнительные флаги Ticky

Существуют дополнительные флаги, которые могут использоваться для увеличения количества счётчиков ticky и качества профиля.

-ticky-allocd

Отслеживать, сколько раз выделяется каждый тип замыкания.

-ticky-dyn-thunk

Отслеживать выделения динамических замыканий.

-ticky-LNE

Они не выделяются и могут быть очень чувствительными к производительности, поэтому обычно мы не хотим запускать счётчики ticky для них, чтобы избежать ещё худшей производительности для библиотек ticky.

Но иногда информация об этих связующих элементах имеет решающее значение. Поэтому у нас есть флаг для их ticky.

-ticky-tag-checks

Эти фиктивные счётчики содержат:

  • Количество предотвращенных проверок тегов в счётчике входов.
  • «infer» в качестве строки аргумента, чтобы отличить их от обычных счётчиков.
  • Имя переменной, по которой происходит ветвление, а также уникальный идентификатор, представляющий место проверки, так как по одной переменной может происходить несколько ветвлений. Уникальный идентификатор идёт первым, затем переменная. Например: u10_s98c (Main) at nofib/spectral/simple/Main.hs:677:1 in u10 где u10 — переменная, а u10_s98c — уникальный идентификатор, связанный с местом проверки.

Обратите внимание, что обработка этих счётчиков eventlog2html пока не очень эффективна. Поэтому, если вы хотите их проверить, вам придётся использовать текстовый интерфейс.

-ticky-ap-thunk

Это позволяет получить точные счётчики входов для кода, такого как f x y, за счёт увеличения размера кода. Мы делаем это, но не используя предварительно вычисленный стандартный код AP-замыканий.

Профилировщик ticky-ticky GHC предоставляет средство низкого уровня для отслеживания количества входов и выделений конкретных отдельных замыканий. Поскольку для профилирования ticky-ticky требуется определённое знакомство с внутренним устройством GHC, мы переместили документацию на вики-сайт разработчиков GHC. Ознакомьтесь с его обзором вариантов профилирования, который содержит ссылку на страницу профилирования ticky-ticky.

Обратите внимание, что образцы ticky-ticky могут генерироваться в двух форматах: в журнале событий, используя тип события -lT, и в формате простого текстового сводки, используя опцию -r ⟨file⟩. Первый формат имеет преимущество в способности определять динамическое поведение в течение всего жизненного цикла программы. Смотрите Счётчики Ticky для получения подробной информации о типах событий, которые сообщаются.

8.9.2. Понимание вывода профилей ticky-ticky

После получения рендерного профиля вы можете начать понимать поведение выделения памяти вашей программы. Существует два класса счётчиков ticky-ticky.

Счётчики, связанные с именами

Каждый «счётчик, связанный с именем», связан с именем, определённым в результате оптимизатора. Для каждого такого имени существует три возможных счётчика: входы, выделение памяти кучи по названию и объём памяти, используемый для выделения этого имени.

Глобальные счётчики

Каждый «глобальный счётчик» описывает какой-либо аспект всего выполнения программы. Например, один глобальный счётчик отслеживает общее выделение памяти кучи; другой отслеживает выделение для PAP.

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

8.9.3. Информация о счётчиках, связанных с именами

Счётчики, связанные с именами, предоставляют следующую информацию о замыкании.

  • Входы — количество раз, когда замыкание было введено.
  • Выделения — сколько (в байтах) выделяется по этому замыканию.
  • Выделения — как часто замыкание выделяется.
  • Свободные переменные — свободные переменные, захваченные этим замыканием.
  • Аргументы — аргументы, которые принимает замыкание.

Информация о свободных переменных и аргументах кодируется с помощью небольшого DSL.

Классификация

Описание

+

словарь

\>

функция

{C,I,F,D,W}

символ, целое число, число с плавающей точкой, двойная точность, слово

{c,i,f,d,w}

неупакованный аналог

T

неупакованная кортеж

P

другой примитивный тип

p

неупакованный примитивный тип

L

список

E

тип перечисления

S

тип с единственным конструктором

M

тип с несколькими конструкторами

.

другой тип

-

зарезервировано для других, чтобы отметить как «неинтересное»

В частности, обратите внимание, что вы можете использовать профилировщик ticky для просмотра любых вызовов функций с аргументами словаря, найдя в профиле классификатор +. Это указывает на то, что функция не смогла специализироваться по какой-либо причине.

8.9.4. Примеры

Типичное использование ticky-ticky — это создание отчёта ticky с помощью журнала событий, вызвав приложение с аргументами RTS, например так:

app <args> +RTS -l-augT

Это создаст файл журнала событий, который содержит результаты из счётчиков ticky. Этот файл можно вручную просмотреть, как любой обычный журнал событий. Однако для ticky-ticky eventlog2html имеет хорошую поддержку для создания таблиц из этих журналов.

С обновлённой версией eventlog2html это можно сделать просто, вызвав eventlog2html на созданном журнале событий. В примере выше вызов будет eventlog2html app.eventlog Это создаст таблицу, в которой можно искать и сортировать все счётчики ticky в журнале.

8.9.5. Заметки о профилировании ticky

  • Вы можете смешивать модули, скомпилированные с и без -ticky, но вы потеряете данные об выделении и подсчёте из неинструментированных модулей в профиле.
  • Связывание с -ticky оказывает значительное влияние на производительность вашей программы. -ticky подразумевает использование неоптимизированного -debug RTS. Поэтому -ticky не следует использовать для производственных сборок.
  • Компиляция с -ticky не влияет на основные оптимизации вашей программы, так как счётчики вставляются после STG-пайплайна. К этому моменту большинство оптимизаций уже выполнено.
  • При использовании журнала событий можно объединить профилирование ticky-ticky и IPE, так как каждое определение счётчика ticky имеет связанную таблицу информации. Этот адрес можно найти в карте IPE, чтобы определить дополнительную информацию (например, расположение исходного кода) о замыкании.
  • Глобальные счётчики ticky доступны только в текстовом выводе ticky (+RTS -r). Но этот режим имеет некоторые ограничения (например, по ширине столбцов) и будет содержать сырой JSON-вывод в некоторых столбцах. По этой причине использование подхода на основе журнала событий предпочтительнее, если это возможно.
[1]

-hi профилирование доступно с обычным временем выполнения, но вам нужно скомпилировать с -finfo-table-map, чтобы интерпретировать результаты.

[2]

Обратите внимание, что эта политика немного изменилась в GHC 7.4.1 по сравнению с предыдущими версиями и, возможно, изменится ещё, отзывы приветствуются.

[3]

Эта функция была добавлена в GHC 7.4.1.

© 2002–2007 The University Court of the University of Glasgow. All rights reserved.
Licensed under the Glasgow Haskell Compiler License.
https://downloads.haskell.org/~ghc/9.12.1/docs/users_guide/profiling.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API