Haskell GHC解释器中冗余使用seq的空间泄漏
我在解释器中键入此代码,内存会迅速消耗:Haskell GHC解释器中冗余使用seq的空间泄漏,haskell,ghc,space-leak,Haskell,Ghc,Space Leak,我在解释器中键入此代码,内存会迅速消耗: last [1..10^7] `seq` () 我不明白为什么这需要超过O(1)个空间。如果我这样做(应该是一样的,因为Show强制弱头范式,所以seq是多余的?) …很好用 我无法在口译员之外再现这种情况 这是怎么回事 以下是一些测试用例: 注意事项: 通过在解释器中运行,我加载wtf.hs而不编译它,并在ghci中键入wtf 通过编译,我可以ghc——生成wtf.hs&./wtf last可以替换具有严格累加器的sum,或在列表中查找max元
last [1..10^7] `seq` ()
我不明白为什么这需要超过O(1)个空间。如果我这样做(应该是一样的,因为Show强制弱头范式,所以seq是多余的?)
…很好用
我无法在口译员之外再现这种情况
这是怎么回事
以下是一些测试用例: 注意事项:
- 通过在解释器中运行,我加载wtf.hs而不编译它,并在ghci中键入
wtf
- 通过编译,我可以
ghc——生成wtf.hs&./wtf
可以替换具有严格累加器的last
,或在列表中查找max元素的函数,空间泄漏仍然会发生sum
- 我在使用
$时没有见过这种行为代码>而不是
seq
- 我尝试添加一个伪
参数,因为我认为这可能是CAF问题。什么都不会改变()
上的函数可能没有问题,因为我可以使用Enum
和更高版本重现行为,而这些版本根本不使用wtf5
Enum
、Num
或Int
可能没有问题,因为我可以在Integer
和wtf14
中复制没有它们的行为wtf16
*
时遇到了其他共享/空间泄漏问题,我认为@n.m是正确的。
没有强制列表中的值,因此1+1+1+1+。。。thunk最终杀死了太空
我将快速进行测试。我们需要查看整数的
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
在哪里
及
正如所料,last
是完全懒惰的
对于Integer Enum类,我们有一个严格的累加器(显式地)用于enumFrom
。在有界情况下(例如,[1..n]
),它调用enumDeltaToInteger
,然后进入up\u list
,它使用辅助程序展开列表,直到达到其限制
但是go
帮助程序中的up\u列表
在x
中是严格的(请参见与lim
的比较)
当在GHCi中运行时,这一切都没有得到优化,在返回()
之前生成对enumFromTo的简单调用
那么,为什么我们要在seq
案例中保留列表,而不是在常规案例中?常规情况在constrant空间中运行良好,这取决于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.[] @ ())))
这使得它保留了价值
正如我们所看到的,up\u list
的实现在累加器中是严格的(因为它与lim
相比,并且列表是延迟展开的——因此last
应该能够在恒定的空间中使用它)。用手写下这个表达式证实了这一点
执行ghci执行的堆配置文件显示保留的整个列表:
这至少告诉我们,这不是一连串的恶作剧,而是整个名单正在严格建立并一直保持,直到被丢弃
所以,谜团是:在ghci中,什么保留着列表参数的last
,而在ghc中没有
我现在怀疑ghci的一些内部(或细微)细节——我认为这值得一张ghci罚单。这可能与此有关吗?@ChrisWong好吧,如果类型默认是罪魁祸首,那么修复诸如
seq(last[1::Int..10^8])()之类的类型应该已经修复了……我用不同的无限列表尝试了这个示例。使用重复1
时没有空间泄漏,使用[1,1..]
时有空间泄漏。也许是enumFrom
和friends的算法造成的?@n.m.这是因为repeat
产生的无限列表由于共享而占用了恒定的空间。如果您尝试map id(repeat 1)
破坏共享,它将再次泄漏。虽然last(map id(repeat 1))^seq^()
将泄漏,last(map id(repeat())^seq^()
不会泄漏。然而,last(map id(repeat Wtf))^seq^()
其中data Wtf=Wtf
。(用backtick替换“^”)seq只要求它的左参数位于WHNF中。从逻辑上讲,我认为,如果要求seq(last[1..10^9])x的结果是一个空间泄漏,那么要求last[1..10^9]
结果的任何东西都是一个空间泄漏(事实并非如此)*很抱歉,我写的东西与上面讨论中的不同,这个降价陷阱阻止我使用反勾号。我尝试了与enumDeltaInteger相同的元素严格策略,但在ghci中没有任何效果。所以我猜ghci是在装傻。甚至可能是错误。如果它已经达到上限,它将不得不强制计算该值,因此我认为这不是一个可能的解释。我认为枚举或整数与此无关。问题是我不确定Haskell在任何情况下都应该如何工作,所以我很难报告一个bug。也许可以尝试使用-frewrite规则的ghci?
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)
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.[] @ ())))
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.[] @ ())))
let x = case last (enumFromTo 1 n) of _ -> ()