Почему добавление подписи полиморфного типа ухудшает производительность?
Здесь простая функция для вычисления чисел Фибоначчи:
fib :: [Int]
fib = 1 : 1 : zipWith (+) fib (tail fib)
В ghci я могу быстро вычислить серию. Фактически, немного экспериментов показывает, что вычисление выполняется в приблизительно линейном времени.
ghci> last $ take 100000 fib
354224848179261915075 -- takes under a second
Если я изменю сигнатуру типа, которая будет полиморфной, вместо:
fib :: Num a => [a]
fib = 1 : 1 : zipWith (+) fib (tail fib)
Затем алгоритм становится медленнее. Фактически, кажется, что теперь он работает в экспоненциальном времени!
Переключение на подпись полиморфного типа означает, что список полностью пересматривается на каждом этапе? Если да, то почему?
Ответы
Ответ 1
Это из-за перевода словаря.
fib :: [Int]
- константа верхнего уровня.
Но это:
fib :: Num a => [a]
fib = 1 : 1 : zipWith (+) fib (tail fib)
- это только функция верхнего уровня, которая будет применяться к словарю Num
во время выполнения. Например:
fib d = 1 : 1 : zipWith (d.+) (fib d) (tail (fib d))
Итак, если вы скомпилируете это без каких-либо оптимизаций, так что специализация не может произойти, вы получите экспоненциальное временное отображение, поскольку список будет перестроен с нуля, при каждом вызове функции - это не ленивые данные структуры.
Ответ 2
Да, подпись полиморфного типа означает, что она пересчитывается на каждом этапе. Ядро, созданное ghc-7.4.2 при -O2
:
lvl_rcZ :: GHC.Integer.Type.Integer
[GblId, Str=DmdType]
lvl_rcZ = __integer 1
Rec {
PolyFib.fib [Occ=LoopBreaker]
:: forall a_a9W. GHC.Num.Num a_a9W => [a_a9W]
[GblId, Arity=1, Str=DmdType L]
PolyFib.fib =
\ (@ a_aal) ($dNum_aam :: GHC.Num.Num a_aal) ->
GHC.Types.:
@ a_aal
(GHC.Num.fromInteger @ a_aal $dNum_aam lvl_rcZ)
(GHC.Types.:
@ a_aal
(GHC.Num.fromInteger @ a_aal $dNum_aam lvl_rcZ)
(GHC.List.zipWith
@ a_aal
@ a_aal
@ a_aal
(GHC.Num.+ @ a_aal $dNum_aam)
(PolyFib.fib @ a_aal $dNum_aam)
(case PolyFib.fib @ a_aal $dNum_aam of _ {
[] -> GHC.List.tail1 @ a_aal;
: _ xs_abD -> xs_abD
})))
end Rec }
Причина заключается в невозможности кэшировать список чисел Фибоначчи для каждого типа, принадлежащего Num
, а fib
явно является полиморфным значением, поэтому он вообще не кэшируется.
Если вы хотите кэшировать его, по крайней мере, для вычисления в каждом типе, используйте локальный список
pfibs :: Num a => [a]
pfibs = res
where
res = 1 : 1 : zipWith (+) res (tail res)
выполняется кэширование для каждого вычисления (поэтому pfibs !! 500
например, быстро), так как теперь список мономорфен в каждом вычислении. Он по-прежнему будет пересчитан для каждого запроса (если вы не привяжете его к мономорфному имени), но не для каждого отдельного элемента списка.
*PolyFib> pfibs !! 999999 :: Int
-4249520595888827205
(0.31 secs, 137462088 bytes)