аппетиты могли возрасти. И вдруг программа, так тщательно настроенная, взорвалась. За год мы, конечно,
многое позабыли о её внутренностях, искать ошибку было бы гораздо труднее. Впрочем не так безнадёжно:
включаем auto-all, caf-all с флагом prof и смотрим отчёт после флага p.
10.9 Упражнения
• Попытайтесь понять причину утечки памяти в примере с функцией sum2 на уровне STG. Не запоминайте
этот пример, вроде, ага, тут у нас копятся отложенные вычисления в аргументе. Переведите на STG и
посмотрите в каком месте происходит слишком много вызовов let-выражений. Переведите и пример
без утечки памяти, а также промежуточный вариант, который не сработал. Для этого вам понадобится
выразить энергичный образец через функцию seq.
Подсказка: За счёт семантики case-выражений нам не нужно специальных конструкций для того чтобы
реализовать seq в STG:
seq = FUN( a b ->
case a of
x -> b
)
При этом вызов функции seq будет встроен. Необходимо будет заменить в коде все вызовы seq на пра-
вую часть определения (без FUN). Также обратите внимание на то, что плюс не является примитивной
функцией:
plusInt = FUN( ma mb ->
case ma of
I# a -> case mb of
I# b -> case (primitivePlus a b) of
res -> I# res
)
В этой функции всплыла на поверхность одна тонкость. Если бы мы писали это выражение в Haskell,
то мы бы сразу вернули результат (I#(primitivePlus a b)), но мы пишем в STG и конструктор может
принять только атомарное выражение. Тогда мы могли бы подумать и сохранить его по старинке в
let-выражении:
-> let v = primitivePlus a b
in
I# v
Но это не правильное выражение в STG! Конструкция в правой части let-выражения должна быть объ-
ектом кучи, а у нас там простое выражение. Но было бы плохо добавить к нему THUNK, поскольку это
выражение содержит вызов примитивной функции на незапакованных значениях. Эта операция выпол-
няется очень быстро. Было бы плохо создавать для неё специальный объект на куче. Поэтому мы сразу
вычисляем это выражение в третьем case. Эта функция также будет встроенной, необходимо заменить
все вызовы на определение.
• Набейте руку в профилировании, пусть это станет привычкой. Вы долго писали большую программу и
теперь вы можете узнать много подробностей из её жизни, что происходит с ней во время вычисления
кода. Вернитесь к прошлой главе и попрофилируйте разные примеры. В конце главы мы рассматрива-
ли пример с поиском корней, там мы создавали большой список промежуточных результатов и в нём
искали решение. Я говорил, что такие алгоритмы очень эффективны при ленивой стратегии вычис-
лений, но так ли это? Будьте критичны, не верьте на слово, ведь теперь у вас есть инструменты для
проверки моих туманных гипотез.
• Откройте документацию к GHC. Пролистайте её. Проникнитесь уважением к разработчикам GHC. Най-
дите исходники GHC и почитайте их. Посмотрите на Haskell-код, написанный профессионалами. Вы-
берите функцию наугад и попытайтесь понять как она строит свой результат.
Упражнения | 179
• Откройте документацию вновь. Нас интересует глава Profiling. Найдите в разделе профилирование
кучи как выполняется retainer profiling. Это специальный тип профилирования направленный на по-
иск данных, которые удерживают в памяти другие данные (типичный сценарий для утечек памяти).
Разберитесь с этим типом профилирования (флаг hr).
• Постройте систему правил, которая выполняет слияние для списков List, определённых в примере для
прагмы RULES. Сравните показатели производительности с правилами и без (для этого скомпилируйте
дважды с флагом O и без) на тестовом выражении:
main = print $ sumL $
mapL (\x -> x - 1000) $ mapL (+100) $ mapL (*2) $ genL 0 1e6
Функция sumL находит сумму элементов в списке, функция genL генерирует список чисел с единичным
шагом от первого аргумента до второго.
Подсказка: вам нужно воспользоваться такими свойствами (не забудьте о фазах компиляции)
mapL f (mapL g xs)
= ...
foldrL cons nil (mapL f xs)
= ...
• Откройте исходный код Prelude и присмотритесь к различным прагмам. Попытайтесь понять почему
они там используются.
180 | Глава 10: Реализация Haskell в GHC
Глава 11
Ленивые чудеса