【问题标题】:Memory usage oddity (memory leak?)内存使用异常(内存泄漏?)
【发布时间】:2019-12-28 23:32:14
【问题描述】:

我有以下类型和两个相关函数,我打算将它们作为大型列表折叠的一部分进行测量:

类型和访问函数:

data Aggregate a = Aggregate (Maybe a) (a -> Aggregate a)

get :: Aggregate a -> Maybe a
get (Aggregate get' _) = get'

put :: Aggregate a -> a -> Aggregate a
put (Aggregate _ put') = put'

第一个函数:

updateFirst :: Maybe a -> a -> Aggregate a
updateFirst cur val = Aggregate new (updateFirst new)
  where new = mplus cur (Just val)

first :: Aggregate a
first = Aggregate Nothing (updateFirst Nothing)

第二个功能:

updateMinimum :: Ord a => Maybe a -> a -> Aggregate a
updateMinimum cur val = Aggregate new (updateMinimum new)
  where new = min <$> (mplus cur (Just val)) <*> Just val

minimum :: Ord a => Aggregate a
minimum = Aggregate Nothing (updateMinimum Nothing)

函数的编写方式应使内存保持不变。因此,我选择在 GHC 中使用 Strict 语言扩展,这会强制评估 thunk。 Weigh 库用于执行分配测量:

test :: A.Aggregate Double -> Int -> Maybe Double
test agg len = A.get $ F.foldl' A.put agg (take len $ iterate (+0.3) 2.0)

testGroup :: String -> A.Aggregate Double -> Weigh ()
testGroup name agg = sequence_ $ map (\cnt -> func (str cnt) (test agg) cnt) counts
  where
    counts  = map (10^) [0 .. 6]
    str cnt = name ++ (show cnt)

main :: IO ()
main =
  mainWith
    (do setColumns [Case, Allocated, Max, GCs]
        testGroup "fst" A.first
        testGroup "min" A.minimum
    )

Weigh 输出如下:

Case          Allocated          Max  GCs
fst1                304           96    0
fst10             2,248           96    0
fst100           21,688           96    0
fst1000         216,088           96    0
fst10000      2,160,088           96    2
fst100000    21,600,088           96   20
fst1000000  216,000,088           96  207
min1                552           96    0
min10             4,728           96    0
min100           46,488           96    0
min1000         464,088           96    0
min10000      4,967,768           96    4
min100000    49,709,656    6,537,712   44
min1000000  497,226,840  103,345,512  445

为什么 GHC 突然在大小为 10^5 和 10^6 的输入中分配更多内存?我的 GHC 版本是8.4.4

【问题讨论】:

  • 这里的嫌疑人是new = ...,由于懒惰,直到需要时才对其进行评估。在返回结果之前尝试强制执行,如updateFirst cur val = new `seq` Aggregate new (updateFirst new)。或者,爆炸模式也可以工作。 (也许需要更多的强迫,但我会从这个开始。)
  • 包含updateFirst 的模块正在使用Strict 扩展名-我的理解是它会自动强制所有thunk-不是这样吗?
  • 可能是这种情况,如果这也是定义Aggregate 的模块。不过,我会尝试测试它。
  • @DanielLovasko “我的理解是它会自动强制所有 thunk” Strict 扩展比这更卑微。这相当于使模块中定义的所有数据类型的字段严格。严格的字段意味着,每当数据类型的值被评估为 WHNF(例如,作为foldl' 的累加器)时,该字段也将被评估为 WHNF。请注意,特别是 Maybe 不会变得严格,因为它在其他地方定义。也许这就是问题的一部分。尝试强制 Just 内部的值,而不仅仅是外部 Maybe
  • 除了铅笔和纸之外,是否有任何工具可以帮助我找到导致 thunk 创建的表达式?或者更确切地说,GHC 有没有办法打印它创建的所有 thunk?至于Strict,我认为您描述的是StrictData,这使得类型严格,但纯Strict 扩展也应该达到函数参数。

标签: haskell memory-management ghc thunk


【解决方案1】:

Haskell 中的严格注解可以说是"relational"。他们说,每当其他东西被评估为 WHNF 时,必须将某些东西评估为 WHNF (weak head normal form)。

对于函数参数,这意味着在函数应用程序本身被评估为 WHNF 之前,函数参数将被评估为 WHNF。

对于严格字段,这意味着每当包含值被评估为 WHNF 时,该字段将被评估为 WHNF。这对于维护用作累加器的数据类型(例如,用作foldl' 的累加器的数据类型)中的“严格链”很有用。否则,即使包含值保留在 WHNF 中,thunk 也会隐藏在惰性字段后面,并导致空间泄漏。特别是,元组没有严格的组件,并且是累加器中空间泄漏的常见来源。

MaybeJust 构造函数中包含的值也不严格。事实上,这就是问题的根源。 Just 中的值在foldl' 的过程中从未被强制,并且在那里积累了thunk。

为什么Strict 没有阻止这个问题?因为它只影响当前模块中的函数和数据类型定义,而Maybe 是在别处定义的。解决方案是在每次迭代时手动强制 Just 内的值,或者定义自己的 "strict Maybe"

【讨论】:

猜你喜欢
  • 2012-07-24
  • 1970-01-01
  • 1970-01-01
  • 2014-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-02-02
相关资源
最近更新 更多