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. Добавление центров затрат вручную

{-# SCC #-}
Где:

любое выражение

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

Центры затрат — это просто программные аннотации. Если передать компилятору -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 № 176.

SCC означает «Set Cost Centre» («задать центр затрат»). Двойные кавычки можно опустить, если 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 помещается в текущий стек, а счётчик входов для этого стека увеличивается на единицу. Иногда стек также требуется сохранить и восстановить; в частности, когда программа создаёт thunk (отложенное вычисление), текущий стек центров затрат сохраняется в thunk и восстанавливается при его вычислении. Благодаря этому стек центров затрат не зависит от порядка вычислений, используемого GHC во время выполнения.

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

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

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

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

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

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

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

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

-prof

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

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

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

-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
Начиная с версии:

9.4.1

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

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

Например, если имеется следующий код:

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

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

-fprof-late-inline
Начиная с версии:

9.4.1

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

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

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

-fprof-late-overloaded
Начиная с версии:

9.10.1

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

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

-fprof-late-overloaded-calls
Начиная с версии:

9.10.1

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

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

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

-fprof-cafs

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

-fprof-manual
По умолчанию:

включено

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

-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 секунды в остальных случаях. Среда выполнения использует один сигнал таймера для подсчета тактов; этот сигнал таймера используется для управления таймером переключения контекста (Использование параллельного Haskell) и таймером профилирования кучи Параметры RTS для профилирования кучи. Кроме того, профилировщик времени напрямую использует сигнал таймера RTS для записи выборок профиля времени.

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

Значение 0 полностью отключает часы 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. Формат профиля eventlog

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

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

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

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

Чтобы создать профиль кучи для программы:

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

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

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

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

_images/eventlog_profile.png

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

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

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

_images/prof_scc.svg

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

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

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

-hT

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

-hc

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

-hm

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

-hd

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

-hy

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

-he
Версия:

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
Версия:

9.2.1

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

--no-automatic-time-samples
Версия:

9.10.1

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

--automatic-era-increment
Версия:

9.10.1

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

--null-eventlog-writer
Версия:

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⟩

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

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

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

Часто определённая структура данных удерживается цепочкой невычисленных замыканий, но профилирование удерживающих объектов сообщает только о ближайшем из них. Например, A удерживает B, B удерживает C, а C удерживает большую структуру. Объектов B может быть много, а объект 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⟩).
  • Текст программы, стек C, любые данные вне кучи (например, данные, выделенные внешними библиотеками и 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-to-PostScript Рокицкого dvips.

-g

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

-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 в каждом синтаксическом логическом контексте (то есть в охранном выражении, условии или квалификаторе).

HPC отображает оба вида информации двумя основными способами: текстовыми отчётами со сводной статистикой (hpc report) и исходным кодом с цветовой разметкой (hpc markup). Для покрытия логических условий возможны четыре результата для каждого охранного выражения, условия или квалификатора: встречаются значения 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

Наложения — экспериментальная возможность 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 Program Coverage

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. Затем при запуске программы можно собирать результаты профилирования двумя способами.
  • Для использования eventlog параметр -lT будет периодически записывать отсчёты ticky в журнал событий. Это позволяет отслеживать динамику поведения на протяжении всего времени работы программы. Подробности о типах регистрируемых событий см. в разделе Счётчики Ticky. Информацию ticky можно преобразовать в интерактивную таблицу с помощью eventlog2html.
  • Для вывода в устаревшем текстовом формате используется параметр -r ⟨file⟩. Он создаёт текстовую таблицу, содержащую сведения о том, сколько раз сработал каждый счётчик за время выполнения программы.

8.9.1. Дополнительные параметры Ticky

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

-ticky-allocd

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

-ticky-dyn-thunk

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

-ticky-LNE

Для них память не выделяется, и они могут быть очень чувствительны к производительности, поэтому обычно мы не хотим запускать для них счётчики 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 thunk.

Профилировщик 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. Сведения о счётчиках, относящихся к конкретным именам

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

  • Entries — сколько раз было вызвано замыкание.
  • Allocs — сколько памяти (в байтах) выделяется этим замыканием.
  • Allod — как часто выделяется память для замыкания.
  • FVs — свободные переменные, захваченные этим замыканием.
  • Args — аргументы, принимаемые этим замыканием.

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

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

Описание

+

словарь

\>

функция

{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. Этот файл можно вручную просматривать как любой обычный журнал событий. Однако eventlog2html хорошо поддерживает создание таблиц из таких журналов ticky-ticky.

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

8.9.5. Примечания о профилировании ticky

  • Можно сочетать модули, скомпилированные с -ticky и без него, но профиль не будет содержать данные о выделениях памяти и счётчиках для неинструментированных модулей.
  • Компоновка с -ticky заметно снижает производительность программы. -ticky подразумевает использование неоптимизированной версии RTS -debug. Поэтому -ticky не следует использовать для производственных сборок.
  • Сборка с -ticky не влияет на оптимизацию core-кода программы, поскольку счётчики добавляются после конвейера 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.14.1/docs/users_guide/profiling.html

Spec-Zone.ru

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