haskell-notes

аргументы размещаются в памяти. Но это уже высший пилотаж искусства оптимизации на Haskell.

Мы узнали о том как работает сборщик мусора и научились просматривать разные параметры работы

программы. У нас появилось несколько критериев оценки производительности программ: минимум глубоких

очисток и отсутствие горбов на графике изменения кучи. Мы потренировались в охоте за утечками памяти

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

Отметим, что не стоит в каждой медленной программе искать утечку памяти. Так в примере concat у нас не

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

узнали какой.

Также мы познакомились с новыми прагмами оптимизации программ. Это встраиваемые функции INLINE,

правила преобразования выражений RULE и встраиваемые конструкторы UNPACK. Разработчики GHC отмеча-

ют, что грамотное использование прагмы INLINE может существенно повысить скорость программы. Если

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

вычислений при её вызовах.

178 | Глава 10: Реализация Haskell в GHC

Надеюсь, что содержание этой главы упростит понимание программ. Как они вычисляются, куда идёт

память, почему она висит в куче. При оптимизации программ предпочитайте изменение алгоритма перед

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

памяти под сборку мусора нам удалось вытянуть ленивую версию sum, но ведь строгая версия требовала в

100 раз меньше памяти, причём её запросы не зависели от величины списка. Если бы мы остановились на

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

аппетиты могли возрасти. И вдруг программа, так тщательно настроенная, взорвалась. За год мы, конечно,

многое позабыли о её внутренностях, искать ошибку было бы гораздо труднее. Впрочем не так безнадёжно:

включаем autoall, cafall с флагом 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, определённых в примере для

Страницы: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162