В GHC встроена система профилирования времени и использования памяти, которая позволяет ответить на вопросы вроде «почему моя программа работает так медленно?» или «почему моя программа использует так много памяти?». Начнём с описания профилирования времени.
Профилирование времени программы состоит из трёх этапов:
- Повторно скомпилируйте программу для профилирования с параметром
-profи, вероятно, с одним из параметров для добавления автоматических аннотаций: рекомендуется использовать-fprof-late. -
Скомпилировав программу для профилирования, запустите её, чтобы создать профиль. Например, простой профиль времени можно получить, запустив программу с
+RTS -p(см.-p). При этом создаётся файл с именемprog.prof, где ⟨prog⟩ — имя вашей программы (без расширения.exe, если вы работаете в Windows).С помощью разных параметров RTS можно создавать профили различных типов. Различные виды профилей описаны далее в этой главе. Для некоторых профилей после запуска программы требуется дополнительная обработка с помощью специальных инструментов.
- Изучите полученную информацию о профилировании, используйте её для оптимизации программы и при необходимости повторите процесс.
Профилировщик времени измеряет время ЦП, затраченное на выполнение кода 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