Утечка пространства с избыточным использованием seq в интерпретаторе GHC
Я набираю этот код в интерпретаторе, и память быстро расходуется:
last [1..10^7] `seq` ()
Я не могу понять, почему это требует больше, чем O(1) место. Если я только что (что должно быть то же самое, потому что Show заставляет слабую голову нормальную форму, поэтому seq является избыточным?):
last [1..10^7]
... это работает отлично.
Я не могу воспроизвести эту ситуацию за пределами переводчика.
Что тут происходит?
Вот несколько тестов: http://hpaste.org/69234
Что следует отметить:
- Запустив интерпретатор, я загружаю wtf.hs без его компиляции и набираю
wtf<n>
в ghci. - Компилируя, я делаю
ghc --make wtf.hs && ./wtf
, last
можно заменить наsum
со строгим аккумулятором или функцией, которая находит элемент max в списке, и утечка пространства все еще происходит- Я не видел такого поведения при использовании
$!
вместоseq
, - Я пытался добавить манекен
()
параметр, потому что я думал, может быть, это проблема CAF. Ничего не меняет. - Это, вероятно, не проблема с функциями на
Enum
потому что я могу воспроизвести поведение сwtf5
и позже, которые не используютEnum
совсем. - Это, вероятно, не проблема с
Num
,Int
, или жеInteger
потому что я могу воспроизвести поведение без них вwtf14
а такжеwtf16
,
Я попытался воспроизвести проблему с арифметикой Пеано, чтобы вывести списки и целые числа из уравнения (извлекая их в конце 10^9), но столкнулся с другими проблемами совместного использования / утечки пространства, которые я не понимаю при попытке реализовать *
,
2 ответа
Нам нужно взглянуть на случай enumFromTo
для целых чисел, и последний:
last [] = errorEmptyList "last"
last (x:xs) = last' x xs
where last' y [] = y
last' _ (y:ys) = last' y ys
Это определено в GHC.Enum как:
enumFrom x = enumDeltaInteger x 1
enumFromThen x y = enumDeltaInteger x (y-x)
enumFromTo x lim = enumDeltaToInteger x 1 lim
где
enumDeltaInteger :: Integer -> Integer -> [Integer]
enumDeltaInteger x d = x `seq` (x : enumDeltaInteger (x+d) d)
-- strict accumulator, so
-- head (drop 1000000 [1 .. ]
-- works
а также
enumDeltaToInteger :: Integer -> Integer -> Integer -> [Integer]
enumDeltaToInteger x delta lim
| delta >= 0 = up_list x delta lim
| otherwise = dn_list x delta lim
up_list :: Integer -> Integer -> Integer -> [Integer]
up_list x0 delta lim = go (x0 :: Integer)
where
go x | x > lim = []
| otherwise = x : go (x+delta)
last
полностью ленивый, как и ожидалось.
Для класса Integer Enum у нас есть строгий аккумулятор (явно) для enumFrom
, В ограниченном случае (например, [1..n]
), это вызывает enumDeltaToInteger
а затем в up_list
, который использует рабочий, чтобы развернуть список, пока его предел не будет достигнут.
Но up_list
строго в x
в go
помощник (см. сравнение с lim
).
При запуске в GHCi ничего из этого не оптимизируется, приводя к наивным вызовам enumFromTo перед возвратом ()
,
let
it_ax6 :: ()
it_ax6 =
case last
@ GHC.Integer.Type.Integer
(GHC.Enum.enumFromTo
@ GHC.Integer.Type.Integer
GHC.Num.$fEnumInteger
(GHC.Integer.smallInteger 1)
(GHC.Real.^
@ GHC.Integer.Type.Integer
@ GHC.Integer.Type.Integer
GHC.Num.$fNumInteger
GHC.Real.$fIntegralInteger
(GHC.Integer.smallInteger 10)
(GHC.Integer.smallInteger 7)))
of _ -> GHC.Unit.()
in
GHC.Base.thenIO
@ ()
@ [()]
(System.IO.print @ () GHC.Show.$fShow() it_ax6)
(GHC.Base.returnIO
@ [()] (GHC.Types.: @ () it_ax6 (GHC.Types.[] @ ())))
Итак, почему мы сохраняем список в seq
дело, а не в обычном случае? Обычный случай хорошо работает в постоянном пространстве, полагаясь на лень enumFromTo
за Integer
и для last
, Ядро GHCi для этого случая выглядит так:
let {
it_aKj :: GHC.Integer.Type.Integer
[LclId,
Unf=Unf{Src=<vanilla>, TopLvl=False, Arity=0, Value=False,
ConLike=False, Cheap=False, Expandable=False,
Guidance=IF_ARGS [] 170 0}]
it_aKj =
GHC.List.last
@ GHC.Integer.Type.Integer
(GHC.Enum.enumFromTo
@ GHC.Integer.Type.Integer
GHC.Num.$fEnumInteger
(GHC.Integer.smallInteger 1)
(GHC.Real.^
@ GHC.Integer.Type.Integer
@ GHC.Integer.Type.Integer
GHC.Num.$fNumInteger
GHC.Real.$fIntegralInteger
(GHC.Integer.smallInteger 10)
(GHC.Integer.smallInteger 7))) } in
GHC.Base.thenIO
@ ()
@ [()]
(System.IO.print
@ GHC.Integer.Type.Integer GHC.Num.$fShowInteger it_aKj)
(GHC.Base.returnIO
@ [()]
(GHC.Types.:
@ ()
(it_aKj
`cast` (UnsafeCo GHC.Integer.Type.Integer ()
:: GHC.Integer.Type.Integer ~ ()))
(GHC.Types.[] @ ())))
Таким образом, они почти идентичны, с различиями:
- в
seq
версия,last (enumFromTo ..)
вынужден внутриcase
, - в обычной версии это ленивый
let
, - в
seq
версия, значение вычисляется, затем отбрасывается, получая()
- ничего не смотрит на результат - в обычном случае это проверяется и показывается.
Странно, что в этом нет ничего волшебного:
let x = case last (enumFromTo 1 n) of _ -> ()
это заставляет его сохранять ценности.
Как видим, реализация up_list
строг в своем аккумуляторе (так как он сравнивается с lim
а список разворачивается лениво - так last
должен быть в состоянии потреблять его в постоянном пространстве). Написание выражения от руки подтверждает это.
Выполнение профиля кучи выполнения ghci показывает весь сохраняемый список:
что говорит нам, по крайней мере, что это не цепочка громов, а весь список строится строго и держится, пока не будет отброшен.
Так что загадка в том, что держит аргумент списка last
в ghci, а не в ghc?
Я подозреваю некоторые внутренние (или тонкие) детали ghci - думаю, это стоит билета ghci.
Я думаю, что @nm прав. Ничто не заставляет значение в списке, так что thunk 1+1+1+1+... в конечном итоге убивает пространство.
Я быстро проведу тест.