case-выражения и декомпозиция в аргументах функции могут стать источником очень неприятных оши-
бок. Программа прошла проверку типов, завелась и вот уже работает-работает как вдруг мы видим на экране:
*** Exception: Prelude. head: empty list
или
*** Exception: Maybe. fromJust: Nothing
И совсем не понятно откуда эта ошибка. В каком модуле сидит эта функция. Может мы её импортировали
из чужой библиотеки или написали сами. Как раз для таких случаев в GHC предусмотрен специальный флаг
xc.
Посмотрим на выполнение такой программы:
Статистика выполнения программы | 171
leak2 6 +RTS -hc
5,944 bytes x seconds
Fri Jun 1 21:51 2012
bytes
30k
(51)PINNED
25k
20k
(72)GHC.IO.Encoding.CAF
15k
(59)GHC.IO.Handle.FD.CAF
10k
(58)GHC.Conc.Signal.CAF
5k
0k
0.0
0.0
0.0
0.1
0.1
0.1
0.1
0.1
0.2
0.2
0.2
seconds
Рис. 10.14: Профиль кучи без утечки памяти
module Main where
addEvens :: Int -> Int -> Int
addEvens a b
| even a && even b = a + b
q = zipWith addEvens [0, 2, 4, 6, 7, 8, 10] (repeat 0)
main = print q
Для того, чтобы воспользоваться флагом xc необходимо скомпилировать программу с возможностью про-
филирования:
$ ghc --make break.hs -rtsopts -prof
$ ./break +RTS -xc
*** Exception (reporting due to +RTS -xc): (THUNK_2_0), stack trace:
Main.CAF
break: break.hs:(4,1)-(5,30): Non-exhaustive patterns in function addEvens
Так мы узнали в каком месте кода проявился злосчастный вызов, это строки (4,1)-(5,30). Что соот-
ветствует определению функции addEvens. Не очень полезная информация. Мы и так бы это узнали. Нам
бы хотелось узнать тот путь, по которому шла программа к этому вызову. Проблема в том, что все вызовы
слились в один CAF для модуля. Так разделим их:
$ ghc --make break.hs -rtsopts -prof -caf-all -auto-all
$ ./break +RTS -xc
*** Exception (reporting due to +RTS -xc): (THUNK_2_0), stack trace:
Main.addEvens,
called from Main.q,
called from Main.CAF:q
--> evaluated by: Main.main,
called from :Main.CAF:main
break: break.hs:(4,1)-(5,30): Non-exhaustive patterns in function addEvens
Теперь мы видим путь к этому вызову, мы пришли в него из знчения q, которое было вызвано из main.
10.7 Оптимизация программ
В этом разделе мы поговорим о том этапе компиляции, на котором происходят преобразования Core ->
Core. Мы называли этот этап упрощением программы.
172 | Глава 10: Реализация Haskell в GHC
Флаги оптимизации
Мы можем задавать степень оптимизации программы специальными флагами. Самые простые флаги на-
чинаются с большой буквы O. Естесственно, чем больше мы оптимизируем, тем дольше компилируется код.
Поэтому не стоит увлекаться оптимизацией на начальном этапе проектирования. Посмотрим какие возмож-
ности у нас есть:
• без -O – минимум оптимизаций, код компилируется как можно быстрее.
• -O0 – выключить оптимизацию полностью
• -O – умеренная оптимизация.
• O2 – активная оптимизация, код компилируется дольше, но пока O2 не сильно выигрывает у O по про-
дуктивности.
Для оптимизации мы компилируем программу с заданным флагом, например попробуйте скомпилиро-
вать самый первый пример с флагом O:
ghc --make sum.hs -O
и утечка памяти исчезнет.
Посмотреть описание конкретных шагов оптимизации можно в документации к GHC. Например при вклю-
чённой оптимизации GHC применяет анализ строгости. В ходе него GHC может исправить простые утечки
памяти за нас. Стоит отметить оптимизацию -fexcess-precision, он может существенно ускорить програм-
мы, в которых много вычислений с Double. Но при этом вычисления могут потерять в точности, округление
становится непредсказуемым.
Прагма INLINE
Если мы посмотрим в исходный файл для модуля Prelude, то мы найдём такое определение для компо-
зиции функций:
-- | Function composition.
{-# INLINE (.) #-}
-- Make sure it has TWO args only on the left, so that it inlines
-- when applied to two functions, even if there is no final argument
(. )
:: (b -> c) -> (a -> b) -> a -> c
(. ) f g = \x -> f (g x)
Помимо знакомого нам определения и комментариев мы видим новую прагму INLINE. Она указывает
компилятору на то, что на этапе упрощения программы необходимо заменить вызов функции на её правую
часть. Этот процесс называют встраиванием функций. Замена будет произведена только в случае полного
применения функции, если синтаксическая арность (количество аргументов слева от знака равно) совпадает
с числом переданных в функцию аргументов. Поэтому для GHC есть существенная разница между определе-
ниями:
(. ) f g = \x -> f (g x)
(. ) f g x = f (g x)
Встраиванием функций мы экономим на создании лишних объектов в куче, но при этом код может су-
щественно разбухнуть. GHC пользуется эвристическим алгоритмом при определении когда функцию стоит
встраивать, а когда – нет. По умолчанию GHC проводит встраивание только внутри модуля. Если мы компи-
лируем с флагом O, функции будут встраиваться между модулями. Для этого GHC сохраняет в интерфейсном
файле (с расширением . hi) не только типы функций, но и павые части достаточно кратких функций. Дли-