списка, который хранит такие пары будет зависеть от числа элементов N как 10 N. Тогда как полезная
нагрузка составляет 2 N. С помощью прагмы UNPACK мы можем отказаться от ленивой гибкости в пользу
меньшего расхода памяти. Эта прагма позволяет встраивать
один конструктор в поле другого. Это поле должно быть строгим (с пометкой ! ) и мономорфным (тип поля
должен быть конкретным типом, а не параметром), причём подчинённый тип должен содержать лишь один
конструктор (у него нет альтернатив):
data PairInt = PairInt
{-# UNPACK #-} !Int
{-# UNPACK #-} !Int
Мы конкретизировали поля Pair и сделали их строгими с помощью восклицательных знаков. После этого
значения из конструктора Int будут храниться прямо в конструкторе PairInt:
nil = []
— глобальный объект (не в счёт)
let p
= PairInt 1 2
— 3 слова
val = Cons p nil
— 3 слова
in
val
————
— 6 слов
Так мы сократим размер до 6 N. Но мы можем пойти ещё дальше. Если этот тип является ключевым
типом нашей программы и мы расчитываем на то, что в нём будет хранится много значений мы можем
создать специальный список для таких пар и распаковать значение списка:
data ListInt = ConsInt {-# UNPACK #-} !PairInt
| NilInt
nil = NilInt
let val = ConsInt 1 2 nil
— 4 слова
in
val
————
— 4 слова
Значение будет встроено дважды и получится, что у нашего нового конструктора Cons уже три поля.
Отметим, что эта прагма имеет смысл лишь при включённом флаге оптимизации -O или выше. Если мы
не включим этот флаг, то компилятор не будет проводить встраивание функций, поэтому при вычислении
функций вроде
Оптимизация программ | 177
sumPair :: PairInt -> Int
sumPair (Pair a b) = a + b
Плюс не будет встроен и вместо того, чтобы сразу сложить два числа с помощью примитивной функции,
компилятор сначала запакует их в конструктор I# и затем применит функцию +, в которой опять распакует
их, сложит и затем, снова запаковав, вернёт результат.
Компилятор автоматически запаковывает все такие значения при передаче в ленивую функцию, это мо-
жет привести к снижению быстродействия даже при включённом флаге оптимизации, при недостаточном
встраивании. Это необходимо учитывать. В таких случая проводите профилирование, убедитесь в том, что
оптимизация привела к повышению эффективности.
В стандартных библиотеках предусмотрено много незапакованных типов. Например это специальные
кортежи. Они пишутся с решётками:
newtype ST s a = ST (STRep s a)
type STRep s a = State# s -> (# State# s, a #)
Это определение типа ST. Специальные кортежи используются для возврата нескольких значений напря-
мую, без создания промежуточного кортежа в куче. В этом случае значения будут сохранены в регистрах
или на стеке. Для использования специальных значений необходимо активировать расширения MagicHash и
UnboxedTuples
Разработчики различных библиотек могут предоставлять несколько вариантов своих данных: ленивые
версии и незапакованные. Например в ST-массив незапакованных значений STUArray s i a эквивалентен
массиву значений в C. В таком массиве можно хранить лишь примитивные типы.
10.8 Краткое содержание
Эта глава была посвящена компилятору GHC. Мы говорим Haskell подразумеваем GHC, говорим GHC
подразумеваем Haskell. К сожалению на данный момент у этого компилятора нет достойных конкурентов.
А может и к счастью, ведь если бы не было GHC, у нас была бы бурная конкуренция среди компиляторов
поплоше. Мы бы не знали, что они не так хороши. Но у нас не было бы программ, которые способны тягаться
по скорости с С. И мы бы говорили: ну декларативное программирование, что поделаешь, за радость аб-
стракций приходится платить. Но есть GHC! Всё-таки это очень трудно: написать компилятор для ленивого
языка
Отметим другие компиляторы: Hugs разработан Марком Джонсом (написан на C), nhc98 основанный
Николасом Райомо (Niklas Rojemo) этот компилятор задумывался как легковесный и простой в установке, он
разрабатывался при поддержке NUTEK, Йоркского университета и Технического университета Чалмерса. От
этого компилятора отпочковался YHC, Йоркский компилятор. UHC – компилятор Утрехтского университета,
разработан для тестирования интересных идей в теории типов. JHC (Джон Мичэм, John Meacham) и LHC
(Дэвид Химмельступ и Остин Сипп, David Himmelstrup, Austin Seipp) компиляторы предназначенные для
проведения более сложных оптимизаций программ с помощью преобразований дерева программы.
В этой главе мы узнали как вычисляются программы в GHC. Мы узнали об этапах компиляции. Снача-
ла проводится синтаксический анализ программы и проверка типов, затем код Haskell переводится на язык
Core. Это сильно урезанная версия Haskell. После этого проводятся оптимизации, которые преобразуют де-
рево программы. На последнем этапе Core переводится на ещё более низкоуровневый, но всё ещё функцио-
нальный язык STG, который превращается в низкоуровневый код и исполняется вычислителем. Посмотреть
на текст вашей программы в Core и STG можно с помощью флагов ddump—simpl ddump—stg при этом лучше
воспользоваться флагом ddump—suppress—all для пропуска многочисленных деталей. Хардкорные разработ-
чики Haskell смотрят Core для того чтобы понять насколько строгой оказалась та или иная функция, как