на функции определяется числом узлов в синтаксическом дереве кода её правой части. Директивой INLINE
мы приказываем GHC встроить функцию. Также есть более слабая версия этой прагмы –INELINABLE. Этой
прагмой мы рекомендуем произвести встраивание функции не смотря на её величину.
Задать порог величины функции для встраивания можно с помощью флага -funfolding-use-
threshold=16. Отметим, что если функция не является экспортируемой и используется лишь один раз,
то GHC втроит её в любом случае, поэтому стоит определять списки экспортируемых определений в шапке
модуля, иначе компилятор будет считать, что экспортируются все определения.
Прагма INLINE может стоять в любом месте, где можно было бы объявить тип значения. Так например
можно указать компилятору встраивать методы класса:
Оптимизация программ | 173
instance Monad T where
{-# INLINE return #-}
return = ...
{-# INLINE (>>=) #-}
(>>=)
= ...
Встраивание значений может существенно ускорить программу. Но не стоит венчать каждую экспортиру-
емую функцию прагмой INLINE, возможно GHC встроит их автоматически. Посмотреть какие функции были
встроены можно по определениям, попавшим в файл . hi.
Например если мы скомпилируем такой код с флагом ddump-hi:
module Inline(f, g) where
g :: Int -> Int
g x = x + 2
f :: Int -> Int
f x = g $ g x
то среди прочих определений увидим:
ghc -c -ddump-hi -O Inline. hs
...
f :: GHC.Types.Int -> GHC.Types.Int
{- Arity: 1, HasNoCafRefs, Strictness: U(L)m,
Unfolding: InlineRule (1, True, False)
(\ x :: GHC.Types.Int ->
case x of wild { GHC.Types.I# x1 ->
GHC.Types.I# (GHC.Prim.+# (GHC.Prim.+# x1 2) 2) }) -}
...
В этом виде прочесть функцию не так просто. Ко всем именам добавлены имена модулей. Приведём
вывод к более простому виду с помощью флага dsuppress-all:
ghc -c -ddump-hi -dsuppress-all -O Inline. hs
...
f :: Int -> Int
{- Arity: 1, HasNoCafRefs, Strictness: U(L)m,
Unfolding: InlineRule (1, True, False)
(\ x :: Int -> case x of wild { I# x1 -> I# (+# (+# x1 2) 2) }) -}
...
Мы видим, что все вызовы функции g были заменены. Если вы всё же подозреваете, что GHC не справ-
ляется с встраиванием ваших часто используемых функций и это сказывается, попробуйте добавить к ним
INLINE, но при этом лучше узнать, привело ли это к росту производительности, проверить с помощью про-
филирования.
Отметим также прагму NOINLINE с её помощью мы можем запретить встраивание функции. Эта праг-
ма часто используется при различных трюках с unsafePerformIO, встраивание функции, которая содержит
неконтролируемые побочные эффекты, может повлиять на её результат.
Прагма RULES
Разработчики GHC хотели, чтобы их компилятор был расширяемым и программист мог бы определять
специфические для его приложения правила оптимизации. Для этого была придумана прагма RULES. За счёт
чистоты функций мы можем в очень простом виде выражать инварианты программы. Инвариант – это неко-
торое свойство значения, которое остаётся постоянным при некоторых преобразованиях. Наиболее распро-
странённые инварианты имеют собственные имена. Например, это коммутативность сложения:
forall a b. a + b = b + a
Здесь мы пишем: для любых a и b изменение порядка следования аргументов у (+) не влияет на результат.
С ключевым словом forall мы уже когда-то встречались, когда говорили о типе ST. Помните тип функции
runST? Пример свойства функции map:
forall f g.
map f . map g = map (f . g)
174 | Глава 10: Реализация Haskell в GHC
Это свойство принято называть дистрибутивностью. Мы видим, что функция композиции дистрибутив-
на относительно функции map. Инварианты определяют скрытые закономерности значений. За счёт чистоты
функций мы можем безболезненно заменить в любом месте программы левую часть на правую или наобо-
рот. Оптимизация начинается тогда, когда мы понимаем, что одна из частей может быть вычислена гораздо
эффективнее другой. Так в примере с map выражение справа от знака равно гораздо эффективнее, поскольку
в нём мы не строим промежуточный список. Особенно ярко разница проявляется в энергичной стратегии
вычислений. Или посмотрим на такое совсем простое свойство:
map id = id
Если мы заменим левую часть на правую, то число сэкономленных усилий будет пропорционально длине
списка. Вряд ли программист станет писать такие выражения, однако они могут появиться после выполнения
других оптимизаций, например после многих встраиваний различных функций.
Можно представить, что эти правила являются дополнительными уравнениями в определении функции:
map f []
= []
map f (x:xs)
= f x : map f xs
map id a
= a
map f (map g x) = map (f . g) x
Словно теперь мы можем проводить сопоставление с образцом не только по конструкторам, но и по выра-
жениям самого языка и функция map стала конструктором. Что интересно, зависимости могут быть какими
угодно, они могут выражать закономерности, присущие той области, которую мы описываем. В дополни-