STUArray s i e - утечка пространства только тогда, когда я == Int?
Меня смущает поведение следующего отрубленного:
import Data.Int
import Data.Array.ST
import Control.Monad.ST
{-# INLINE fib #-}
fib _ 0 = return 0
fib _ 1 = return 1
fib c n = do
f1 <- memo c (fib c) (n-1)
f2 <- memo c (fib c) (n-2)
return (f1+f2)
newtype C a = C a
{-# INLINE memo #-}
memo (C a) f k = do
e <- readArray a k
if e == (-1)
then do
v <- f k
writeArray a k v
return v
else return e
evalFib :: Int -> Int
evalFib n = runST $ do
a <- newArray (0,n) (-1) :: ST s (STUArray s Int Int)
fib (C a) n
main = print $ evalFib 120000
Когда скомпилировано с -O2
переполнение стека (показывает 20M используемой памяти). Заблуждение заключается в том, что на самом деле он работает должным образом (без переполнения стека и использования 9М памяти), если выполнено любое из следующих изменений:
Int64
используется вместоInt
: (даваяevalFib :: Int64 -> Int
а такжеSTUArray s Int64 Int
). На самом деле любойInt*
(Int32
,Int16
и т. д.) будет делать то же самое, что иWord
или жеWord*
;newtype C a
удаляется с картинки;data C a = C !a
используется вместоnewtype
Я пытаюсь разобраться в этом поведении: это ошибка в модуле GHC/ массив (он показывает идентичное поведение в 7.4.2
а также 7.6.2
) или так должно работать?
PS Самое смешное, что когда я пытаюсь скомпилировать ghc array.hs -O2 -fext-core
чтобы увидеть разницу в ядре, обе версии GHC перестали работать с "ghc: panic! (произошло" невозможное ")". Не повезло и здесь..
2 ответа
Глядя на ядро из 7.6.1, с -O2
а также -dsuppress-uniques
, функция, которая делает работу, Main.main_$spoly_$wa
структурно (почти) идентичен ли я int
или же Int64
как тип индекса. Поскольку ядро длинное и сложное, вот diff
выход:
$ diff Int_core.dump-simpl Int64_core.dump-simpl
11,12c11,12
< (Data.Array.Base.STUArray s GHC.Types.Int GHC.Types.Int)
< (Main.C (Data.Array.Base.STUArray s GHC.Types.Int GHC.Types.Int))
---
> (Data.Array.Base.STUArray s GHC.Int.Int64 GHC.Types.Int)
> (Main.C (Data.Array.Base.STUArray s GHC.Int.Int64 GHC.Types.Int))
26,27c26,27
< (Data.Array.Base.STUArray s GHC.Types.Int GHC.Types.Int)
< (Main.C (Data.Array.Base.STUArray s GHC.Types.Int GHC.Types.Int)))
---
> (Data.Array.Base.STUArray s GHC.Int.Int64 GHC.Types.Int)
> (Main.C (Data.Array.Base.STUArray s GHC.Int.Int64 GHC.Types.Int)))
Разные типы индексов, они, конечно, разные.
33,40d32
< l :: GHC.Types.Int
< [LclId]
< l = GHC.Types.I# sc } in
< let {
< u :: GHC.Types.Int
< [LclId]
< u = GHC.Types.I# sc1 } in
< let {
Для типа индекса Int
GHC выдает несколько более информативные ошибки для индексов, выходящих за границы, для этого требуется нижняя и верхняя граница допустимых индексов. (Реализация по умолчанию index
не отменяется в instance Ix Int64
.)
45,46c37
< GHC.Types.False ->
< case poly_$w$j5 (GHC.Types.I# a) l u of wild2 { };
---
> GHC.Types.False -> case GHC.Arr.hopelessIndexError of wild1 { };
Другая ошибка, indexError
против hopelessIndexError
, Следующие различия также касаются только ошибок индекса.
49,50c40
< GHC.Types.False ->
< case poly_$w$j5 (GHC.Types.I# a) l u of wild2 { };
---
> GHC.Types.False -> case GHC.Arr.hopelessIndexError of wild2 { };
58c48
< case poly_$w$j4 y (GHC.Types.I# sc2) of wild3 { };
---
> case poly_$w$j3 y (GHC.Types.I# sc2) of wild4 { };
62c52
< case poly_$w$j4 y (GHC.Types.I# sc2) of wild5 { };
---
> case poly_$w$j3 y (GHC.Types.I# sc2) of wild5 { };
77,78c67
< GHC.Types.False ->
< case poly_$w$j3 (GHC.Types.I# a1) l u of wild6 { };
---
> GHC.Types.False -> case GHC.Arr.hopelessIndexError of wild6 { };
81,82c70
< GHC.Types.False ->
< case poly_$w$j3 (GHC.Types.I# a1) l u of wild7 { };
---
> GHC.Types.False -> case GHC.Arr.hopelessIndexError of wild7 { };
Теперь еще раз другой тип индекса:
110c98
< GHC.Types.Int
---
> GHC.Int.Int64
152c140
< s GHC.Types.Int GHC.Types.Int>)>)
---
> s GHC.Int.Int64 GHC.Types.Int>)>)
И наконец, 0
а также 1
получили разные имена верхнего уровня.
177,178c165,166
< 0 -> (# sc5, lvl5 #);
< 1 -> (# sc5, lvl6 #)
---
> 0 -> (# sc5, lvl #);
> 1 -> (# sc5, lvl1 #)
Таким образом, весь код, который выполняет реальную работу, идентичен. Тем не менее, один вызывает переполнение стека (хотя только -K9M
достаточно [-K8731K
здесь достаточно, -K8730K
нет]) а другой нет.
Разница действительно вызвана ошибками индекса. Код с Int
Индексы выделяет два в штучной упаковке Int
в каждом рекурсивном вызове, что Int64
код не выделяется, потому что
Main.main_$spoly_$wa [Occ=LoopBreaker]
:: forall s.
GHC.Prim.Int#
-> GHC.Prim.Int#
-> GHC.Prim.Int#
-> GHC.Prim.MutableByteArray# s
-> (GHC.Prim.~#)
*
(Data.Array.Base.STUArray s GHC.Types.Int GHC.Types.Int)
(Main.C (Data.Array.Base.STUArray s GHC.Types.Int GHC.Types.Int))
-> GHC.Prim.Int#
-> GHC.Prim.State# s
-> (# GHC.Prim.State# s, GHC.Types.Int #)
содержит две ссылки на массив.
Это использует больше стека, и эти в штучной упаковке Int
Должны быть собраны мусор, что приводит к гораздо большим показателям GC. Кроме того, thunk для ошибки индекса немного больше, чем hopelessIndexError
санк.
Теперь, если вы поможете компилятору
- удаление оболочки нового типа
- сделать функцию строгой в массиве (по шаблонам взрыва или
data C a = C !a
)
или некоторыми другими способами, он производит лучший код, который управляет без переполнения стека для данного аргумента, поскольку существует только одна ссылка на массив в работнике, и, таким образом, распределение в штучной упаковке Int
для границ не нужно.
Обратите внимание, что этот алгоритм вызывает переполнение стека для немного больших аргументов даже с помощью компилятора.
Ваша первоначальная программа, как вы говорите, переполняется стеком:
$ ghc -O2 A.hs --make -rtsopts
$ ./A +RTS -s
Stack space overflow: current size 8388608 bytes.
Use `+RTS -Ksize -RTS' to increase it.
21,213,928 bytes allocated in the heap
3,121,864 bytes copied during GC
5,400,592 bytes maximum residency (4 sample(s))
20,464 bytes maximum slop
20 MB total memory in use (0 MB lost due to fragmentation)
Хотя, если мы перейдем на Int64, он будет работать нормально:
$ time ./A
-2092835058118682368
./A 0.03s user 0.01s system 92% cpu 0.050 total
Так где же утечка?
Просто визуальная проверка вашего кода, есть очевидная проблема:
- fib ленив в аргументе массива
Вы также можете сделать вывод из поведения, которое вы видели:
newtype C a is removed from the picture;
data C a = C !a is used instead of newtype
Все из которых сервер, чтобы выставить массив для анализатора строгости.
Если вы сделаете FIB строгим в массиве:
{-# LANGUAGE BangPatterns #-}
{-# INLINE fib #-}
fib !_ 0 = return 0
fib !_ 1 = return 1
fib !c n = do
f1 <- memo c (fib c) (n-1)
f2 <- memo c (fib c) (n-2)
return (f1+f2)
Тогда это просто работает:
$ time ./A
-2092835058118682368
./A 0.03s user 0.01s system 89% cpu 0.052 total
Так почему же он протекает с одним типом, а не с другим? Я думаю, вам просто повезло с Int64. Но я бы посчитал это "ошибкой", вероятно, в правилах перезаписи для различных типов num.
Посмотрев на вывод упростителя, мы получим совершенно другой прогон правил перезаписи в случае Int64 и Int. Поскольку базовые функции часто индексируются Int, в конечном итоге вы выполняете различные оптимизации, используя общий тип Int. В этом случае достаточно спутать анализатор строгости.
Как всегда, применяются общие правила: дайте компилятору больше советов, и вы получите лучший код.