Ответ 1
Эта страница GHC Trac также хорошо объясняет проходы. Эта страница объясняет порядок оптимизации, хотя, как и большинство Wiki Trac, она устарела.
Для специфики лучше всего, наверное, посмотреть, как скомпилирована конкретная программа. Лучший способ увидеть, какие оптимизации выполняются, - это скомпилировать программу, используя флаг -v
. В качестве примера можно привести первую часть Haskell, которую я смог найти на своем компьютере:
Glasgow Haskell Compiler, Version 7.4.2, stage 2 booted by GHC version 7.4.1
Using binary package database: /usr/lib/ghc-7.4.2/package.conf.d/package.cache
wired-in package ghc-prim mapped to ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7
wired-in package integer-gmp mapped to integer-gmp-0.4.0.0-af3a28fdc4138858e0c7c5ecc2a64f43
wired-in package base mapped to base-4.5.1.0-6e4c9bdc36eeb9121f27ccbbcb62e3f3
wired-in package rts mapped to builtin_rts
wired-in package template-haskell mapped to template-haskell-2.7.0.0-2bd128e15c2d50997ec26a1eaf8b23bf
wired-in package dph-seq not found.
wired-in package dph-par not found.
Hsc static flags: -static
*** Chasing dependencies:
Chasing modules from: *SleepSort.hs
Stable obj: [Main]
Stable BCO: []
Ready for upsweep
[NONREC
ModSummary {
ms_hs_date = Tue Oct 18 22:22:11 CDT 2011
ms_mod = main:Main,
ms_textual_imps = [import (implicit) Prelude, import Control.Monad,
import Control.Concurrent, import System.Environment]
ms_srcimps = []
}]
*** Deleting temp files:
Deleting:
compile: input file SleepSort.hs
Created temporary directory: /tmp/ghc4784_0
*** Checking old interface for main:Main:
[1 of 1] Compiling Main ( SleepSort.hs, SleepSort.o )
*** Parser:
*** Renamer/typechecker:
*** Desugar:
Result size of Desugar (after optimization) = 79
*** Simplifier:
Result size of Simplifier iteration=1 = 87
Result size of Simplifier iteration=2 = 93
Result size of Simplifier iteration=3 = 83
Result size of Simplifier = 83
*** Specialise:
Result size of Specialise = 83
*** Float out(FOS {Lam = Just 0, Consts = True, PAPs = False}):
Result size of Float out(FOS {Lam = Just 0,
Consts = True,
PAPs = False}) = 95
*** Float inwards:
Result size of Float inwards = 95
*** Simplifier:
Result size of Simplifier iteration=1 = 253
Result size of Simplifier iteration=2 = 229
Result size of Simplifier = 229
*** Simplifier:
Result size of Simplifier iteration=1 = 218
Result size of Simplifier = 218
*** Simplifier:
Result size of Simplifier iteration=1 = 283
Result size of Simplifier iteration=2 = 226
Result size of Simplifier iteration=3 = 202
Result size of Simplifier = 202
*** Demand analysis:
Result size of Demand analysis = 202
*** Worker Wrapper binds:
Result size of Worker Wrapper binds = 202
*** Simplifier:
Result size of Simplifier = 202
*** Float out(FOS {Lam = Just 0, Consts = True, PAPs = True}):
Result size of Float out(FOS {Lam = Just 0,
Consts = True,
PAPs = True}) = 210
*** Common sub-expression:
Result size of Common sub-expression = 210
*** Float inwards:
Result size of Float inwards = 210
*** Liberate case:
Result size of Liberate case = 210
*** Simplifier:
Result size of Simplifier iteration=1 = 206
Result size of Simplifier = 206
*** SpecConstr:
Result size of SpecConstr = 206
*** Simplifier:
Result size of Simplifier = 206
*** Tidy Core:
Result size of Tidy Core = 206
writeBinIface: 4 Names
writeBinIface: 28 dict entries
*** CorePrep:
Result size of CorePrep = 224
*** Stg2Stg:
*** CodeGen:
*** CodeOutput:
*** Assembler:
'/usr/bin/gcc' '-fno-stack-protector' '-Wl,--hash-size=31' '-Wl,--reduce-memory-overheads' '-I.' '-c' '/tmp/ghc4784_0/ghc4784_0.s' '-o' 'SleepSort.o'
Upsweep completely successful.
*** Deleting temp files:
Deleting: /tmp/ghc4784_0/ghc4784_0.c /tmp/ghc4784_0/ghc4784_0.s
Warning: deleting non-existent /tmp/ghc4784_0/ghc4784_0.c
link: linkables are ...
LinkableM (Sat Sep 29 20:21:02 CDT 2012) main:Main
[DotO SleepSort.o]
Linking SleepSort ...
*** C Compiler:
'/usr/bin/gcc' '-fno-stack-protector' '-Wl,--hash-size=31' '-Wl,--reduce-memory-overheads' '-c' '/tmp/ghc4784_0/ghc4784_0.c' '-o' '/tmp/ghc4784_0/ghc4784_0.o' '-DTABLES_NEXT_TO_CODE' '-I/usr/lib/ghc-7.4.2/include'
*** C Compiler:
'/usr/bin/gcc' '-fno-stack-protector' '-Wl,--hash-size=31' '-Wl,--reduce-memory-overheads' '-c' '/tmp/ghc4784_0/ghc4784_0.s' '-o' '/tmp/ghc4784_0/ghc4784_1.o' '-DTABLES_NEXT_TO_CODE' '-I/usr/lib/ghc-7.4.2/include'
*** Linker:
'/usr/bin/gcc' '-fno-stack-protector' '-Wl,--hash-size=31' '-Wl,--reduce-memory-overheads' '-o' 'SleepSort' 'SleepSort.o' '-L/usr/lib/ghc-7.4.2/base-4.5.1.0' '-L/usr/lib/ghc-7.4.2/integer-gmp-0.4.0.0' '-L/usr/lib/ghc-7.4.2/ghc-prim-0.2.0.0' '-L/usr/lib/ghc-7.4.2' '/tmp/ghc4784_0/ghc4784_0.o' '/tmp/ghc4784_0/ghc4784_1.o' '-lHSbase-4.5.1.0' '-lHSinteger-gmp-0.4.0.0' '-lgmp' '-lHSghc-prim-0.2.0.0' '-lHSrts' '-lm' '-lrt' '-ldl' '-u' 'ghczmprim_GHCziTypes_Izh_static_info' '-u' 'ghczmprim_GHCziTypes_Czh_static_info' '-u' 'ghczmprim_GHCziTypes_Fzh_static_info' '-u' 'ghczmprim_GHCziTypes_Dzh_static_info' '-u' 'base_GHCziPtr_Ptr_static_info' '-u' 'base_GHCziWord_Wzh_static_info' '-u' 'base_GHCziInt_I8zh_static_info' '-u' 'base_GHCziInt_I16zh_static_info' '-u' 'base_GHCziInt_I32zh_static_info' '-u' 'base_GHCziInt_I64zh_static_info' '-u' 'base_GHCziWord_W8zh_static_info' '-u' 'base_GHCziWord_W16zh_static_info' '-u' 'base_GHCziWord_W32zh_static_info' '-u' 'base_GHCziWord_W64zh_static_info' '-u' 'base_GHCziStable_StablePtr_static_info' '-u' 'ghczmprim_GHCziTypes_Izh_con_info' '-u' 'ghczmprim_GHCziTypes_Czh_con_info' '-u' 'ghczmprim_GHCziTypes_Fzh_con_info' '-u' 'ghczmprim_GHCziTypes_Dzh_con_info' '-u' 'base_GHCziPtr_Ptr_con_info' '-u' 'base_GHCziPtr_FunPtr_con_info' '-u' 'base_GHCziStable_StablePtr_con_info' '-u' 'ghczmprim_GHCziTypes_False_closure' '-u' 'ghczmprim_GHCziTypes_True_closure' '-u' 'base_GHCziPack_unpackCString_closure' '-u' 'base_GHCziIOziException_stackOverflow_closure' '-u' 'base_GHCziIOziException_heapOverflow_closure' '-u' 'base_ControlziExceptionziBase_nonTermination_closure' '-u' 'base_GHCziIOziException_blockedIndefinitelyOnMVar_closure' '-u' 'base_GHCziIOziException_blockedIndefinitelyOnSTM_closure' '-u' 'base_ControlziExceptionziBase_nestedAtomically_closure' '-u' 'base_GHCziWeak_runFinalizzerBatch_closure' '-u' 'base_GHCziTopHandler_flushStdHandles_closure' '-u' 'base_GHCziTopHandler_runIO_closure' '-u' 'base_GHCziTopHandler_runNonIO_closure' '-u' 'base_GHCziConcziIO_ensureIOManagerIsRunning_closure' '-u' 'base_GHCziConcziSync_runSparks_closure' '-u' 'base_GHCziConcziSignal_runHandlers_closure'
link: done
*** Deleting temp files:
Deleting: /tmp/ghc4784_0/ghc4784_1.o /tmp/ghc4784_0/ghc4784_0.s /tmp/ghc4784_0/ghc4784_0.o /tmp/ghc4784_0/ghc4784_0.c
*** Deleting temp dirs:
Deleting: /tmp/ghc4784_0
Взглянув с первого *** Simplifier:
на последний, где все фазы оптимизации происходят, мы видим довольно много.
Прежде всего, Упроститель работает между почти всеми фазами. Это значительно упрощает запись многих пропусков. Например, при реализации многих оптимизаций они просто создают правила перезаписи для распространения изменений вместо того, чтобы делать это вручную. Упрощение включает в себя ряд простых оптимизаций, включая встраивание и слияние. Основным ограничением этого, что я знаю, является то, что GHC отказывается от встроенных рекурсивных функций и что для правильной работы вещи нужно правильно называть.
Далее мы видим полный список всех выполненных оптимизаций:
-
Специализируются
Основная идея специализации заключается в удалении полиморфизма и перегрузки путем определения мест, где вызывается функция, и создания версий функции, которые не являются полиморфными, - они специфичны для типов, с которыми они вызываются. Вы также можете сообщить компилятору об этом с помощью
SPECIALISE
прагмы. В качестве примера возьмем факторную функцию:fac :: (Num a, Eq a) => a -> a fac 0 = 1 fac n = n * fac (n - 1)
Поскольку компилятор не знает каких-либо свойств используемого умножения, он вообще не может его оптимизировать. Если, однако, он видит, что он используется на
Int
, теперь он может создать новую версию, отличающуюся только типом:fac_Int :: Int -> Int fac_Int 0 = 1 fac_Int n = n * fac_Int (n - 1)
Далее, правила, упомянутые ниже, могут запускаться, и вы получаете что-то, работающее на unboxed
Int
s, которое намного быстрее оригинала. Еще один способ взглянуть на специализацию - это частичное приложение для словарей типа и переменных типа.источник здесь загружает ноты.
-
Поплавок
EDIT: Я, по-видимому, неправильно понял это раньше. Мое объяснение полностью изменилось.
Основная идея состоит в том, чтобы переместить вычисления, которые не должны повторяться из функций. Например, предположим, что у нас было это:
\x -> let y = expensive in x+y
В приведенной выше лямбде каждый раз, когда вызывается функция,
y
пересчитывается. Лучшая функция, которая производит плавание, составляетlet y = expensive in \x -> x+y
Чтобы облегчить процесс, могут быть применены другие преобразования. Например, это происходит:
\x -> x + f 2 \x -> x + let f_2 = f 2 in f_2 \x -> let f_2 = f 2 in x + f_2 let f_2 = f 2 in \x -> x + f_2
Снова повторяется повторное вычисление.
Источник в этом случае очень читаем.
В настоящий момент привязки между двумя соседними лямбдами не плавают. Например, этого не происходит:
\x y -> let t = x+x in ...
будет
\x -> let t = x+x in \y -> ...
-
Поплавок внутрь
Цитата исходного кода,
Основная цель
floatInwards
плавает в ветвях корпуса, поэтому мы не выделяем вещи, не сохраняем их в стеке, а затем обнаруживаем, что они не нужны в выбранной ветке.В качестве примера предположим, что мы имеем это выражение:
let x = big in case v of True -> x + 1 False -> 0
Если
v
оценивается какFalse
, то, выделяяx
, предположительно какой-то большой кусок, мы потратили впустую время и пространство. Плавающий внутрь исправляет это, производя это:case v of True -> let x = big in x + 1 False -> let x = big in 0
который впоследствии заменяется упростителем с
case v of True -> big + 1 False -> 0
Эта статья, хотя и охватывает другие темы, дает довольно четкое представление. Обратите внимание, что, несмотря на их имена, плавающие и плавающие не попадают в бесконечный цикл по двум причинам:
- Float in floats позволяет в операторах
case
, а float out - с функциями. - Существует фиксированный порядок проходов, поэтому они не должны чередоваться бесконечно.
- Float in floats позволяет в операторах
-
Анализ спроса
Анализ спроса или анализ строгости - это не просто трансформация, а больше, как, например, название, означает пропуск сбора информации. Компилятор находит функции, которые всегда оценивают свои аргументы (или, по крайней мере, некоторые из них), и передают эти аргументы, используя call-by-value, а не call-by-need. Поскольку вы избегаете накладных расходов, это часто намного быстрее. Многие проблемы с производительностью в Haskell возникают из-за сбоя этого прохода, или кода просто недостаточно строго. Простым примером является различие между использованием
foldr
,foldl
иfoldl'
, чтобы суммировать список целых чисел - первый вызывает переполнение стека, второе вызывает переполнение кучи, а последнее работает нормально, из-за строгости. Это, вероятно, самый простой способ понять и лучше всего документировать из всех этих. Я считаю, что полиморфизм и код CPS часто побеждают это. -
Рабочий Wrapper связывает
Основная идея преобразования worker/wrapper заключается в том, чтобы сделать жесткий цикл на простой структуре, конвертируя ее в и из этой структуры на концах. Например, возьмите эту функцию, которая вычисляет факториал числа.
factorial :: Int -> Int factorial 0 = 1 factorial n = n * factorial (n - 1)
Используя определение
Int
в GHC, имеемfactorial :: Int -> Int factorial (I# 0#) = I# 1# factorial (I# n#) = I# (n# *# case factorial (I# (n# -# 1#)) of I# down# -> down#)
Обратите внимание, как код распространяется в
I#
s? Мы можем удалить их, выполнив следующие действия:factorial :: Int -> Int factorial (I# n#) = I# (factorial# n#) factorial# :: Int# -> Int# factorial# 0# = 1# factorial# n# = n# *# factorial# (n# -# 1#)
Хотя этот конкретный пример также мог быть выполнен с помощью SpecConstr, преобразование рабочего/обертки является очень общим в том, что он может сделать.
-
Общее подвыражение
Это еще одна очень простая оптимизация, которая очень эффективна, как анализ строгости. Основная идея состоит в том, что если у вас есть два выражения, которые одинаковы, они будут иметь одинаковое значение. Например, если
fib
является калькулятором числа Фибоначчи, CSE преобразуетfib x + fib x
в
let fib_x = fib x in fib_x + fib_x
который сокращает вычисление пополам. К сожалению, это может иногда мешать другим оптимизации. Другая проблема заключается в том, что два выражения должны быть в одном месте и что они должны быть синтаксически одинаковыми, а не одинаковыми по значению. Например, CSE не будет запускаться в следующем коде без связки:
x = (1 + (2 + 3)) + ((1 + 2) + 3) y = f x z = g (f x) y
Однако, если вы скомпилируете через llvm, вы можете получить некоторые из этих объединений из-за своего пропущенного номера глобальной стоимости.
-
Освободите дело
Это, кажется, ужасно документированное преобразование, помимо того факта, что это может вызвать взрыв кода. Вот переформатированная (и слегка переписанная) версия небольшой документации, которую я нашел:
Этот модуль проходит
Core
и ищетcase
для свободных переменных. Критерием является: если на пути к рекурсивному вызову естьcase
на свободной переменной, то рекурсивный вызов заменяется разворачиванием. Например, вf = \ t -> case v of V a b -> a : f t
внутренний
f
заменяется. сделатьf = \ t -> case v of V a b -> a : (letrec f = \ t -> case v of V a b -> a : f t in f) t
Обратите внимание на необходимость затенения. Упрощая, получаем
f = \ t -> case v of V a b -> a : (letrec f = \ t -> a : f t in f t)
Это лучший код, потому что
a
свободен внутри внутреннегоletrec
, вместо того, чтобы проектировать его изv
. Обратите внимание, что это касается свободных переменных, в отличие от SpecConstr, который имеет дело с аргументами известной формы.См. ниже дополнительную информацию о SpecConstr.
-
SpecConstr - это преобразование программ типа
f (Left x) y = somthingComplicated1 f (Right x) y = somethingComplicated2
в
f_Left x y = somethingComplicated1 f_Right x y = somethingComplicated2 {-# INLINE f #-} f (Left x) = f_Left x f (Right x) = f_Right x
В качестве расширенного примера возьмите это определение
last
:last [] = error "last: empty list" last (x:[]) = x last (x:x2:xs) = last (x2:xs)
Сначала преобразуем его в
last_nil = error "last: empty list" last_cons x [] = x last_cons x (x2:xs) = last (x2:xs) {-# INLINE last #-} last [] = last_nil last (x : xs) = last_cons x xs
Далее, выполняется упрощение, и мы имеем
last_nil = error "last: empty list" last_cons x [] = x last_cons x (x2:xs) = last_cons x2 xs {-# INLINE last #-} last [] = last_nil last (x : xs) = last_cons x xs
Обратите внимание, что программа работает быстрее, поскольку мы не многократно боксируем и распаковываем переднюю часть списка. Также обратите внимание, что вложение имеет решающее значение, поскольку оно позволяет использовать новые более эффективные определения, а также делать рекурсивные определения лучше.
SpecConstr управляется несколькими эвристиками. Те, что упоминаются в документе, таковы:
- Ярлыки явны и arity
a
. - Правая сторона "достаточно мала", что-то контролируется флагом.
- Функция рекурсивна, и специализированный вызов используется в правой части.
- Все аргументы функции присутствуют.
- По крайней мере один из аргументов - это приложение-конструктор.
- Этот аргумент анализируется случайным образом в функции.
Однако эвристика почти наверняка изменилась. Фактически, в документе упоминается альтернативная шестая эвристика:
Специализируйте аргумент
x
, только еслиx
проверяется толькоcase
и не передается обычной функции или не возвращается как часть результата. - Ярлыки явны и arity
Это был очень маленький файл (12 строк), и, возможно, это не вызвало много оптимизаций (хотя я думаю, что все это делало). Это также не говорит вам, почему он выбрал эти пропуски и почему он поместил их в этом порядке.