связке foldrL/mapL.
176 | Глава 10: Реализация Haskell в GHC
Прагма UNPACK
Наш основной враг на этапе оптимизации программы это лишние объекты кучи. Чем меньше объектов
мы создаём на пути к результату, тем эффективнее наша программа. С помощью прагмы INLINE мы можем
избавиться от многих объектов, связанных с вызовом функции, это объекты типа FUN. Прагма UNPACK позволя-
ет нам бороться с лишними объектами типа CON. В прошлой главе мы говорили о том, что значения в Haskell
содержат дополнительную служебную информацию, которая необходима на этапе вычисления, например
значение сначала было отложенным, потом мы до него добрались и вычислили, возможно оно оказалось не
определённым значением (undefined). Такие значения называются запакованными (boxed). Незапакованное
значение, это примитивное значение, как оно представлено в памяти компьютера. Вспомним определение
целых чисел:
data Int = I# Int#
По традиции все незапакованные значения пишутся с решёткой на конце. Запакованные значения позво-
ляют отклдывать вычисления, пользоваться undefined при определении функции. Но за эту гибкость прихо-
дится платить. Вспомним расход памяти в выражении [Pair 1 2]
nil = []
-- глобальный объект (не в счёт)
let x1
= I# 1
-- 2 слова
x2
= I# 2
-- 2 слова
p
= Pair x1 x2
-- 3 слова
val = Cons p nil
-- 3 слова
in
val
------------
-- 10 слов
Получилось десять слов для списка из одного элемента, который фактически хранит два значения. Размер
списка, который хранит такие пары будет зависеть от числа элементов 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. Специальные кортежи используются для возврата нескольких значений напря-