导致内存消耗激增的标准,看不到 CAF

Criterion causing memory consumption to explode, no CAFs in sight

基本上我有一个简单的函数调用, 当与 Criterion 一起使用时,会导致 内存消耗激增。

假设我有以下程序:

{-# OPTIONS_GHC -fno-cse #-}
{-# LANGUAGE BangPatterns #-}
module Main where
import Criterion.Main
import Data.List

num :: Int
num = 10000000

lst :: a -> [Int]
lst _ = [1,2..num]

myadd :: Int -> Int -> Int
myadd !x !y = let !result = x + y in
  result

mysum = foldl' myadd 0

main :: IO ()
main = do
  print $ mysum (lst ())

那么这个程序(用O0编译的)运行没问题,不用 内存爆炸。

如果我们使用 cabal build -v 生成编译转储 调用命令,然后标记 -ddump-simpl -fforce-recomp -O0 -dsuppress-all(建议在 IO/Monadic assign operator causing ghci to explode for infinite list 中)到 ghc --make -no-link ... 命令的末尾,我们得到以下核心:

num
num = I# 10000000

lst
lst = \ @ a_a3Yn _ -> enumFromThenTo $fEnumInt (I# 1) (I# 2) num

myadd
myadd =
  \ x_a3Cx y_a3Cy ->
    case x_a3Cx of x1_X3CC { I# ipv_s4gX ->
    case y_a3Cy of y1_X3CE { I# ipv1_s4h0 ->
    + $fNumInt x1_X3CC y1_X3CE
    }
    }

mysum
mysum = foldl' myadd (I# 0)

main
main =
  print
    $fShowInt (mysum (enumFromThenTo $fEnumInt (I# 1) (I# 2) num))

main
main = runMainIO main

似乎没有生成CAF,这是一致的 事实上,该程序不会爆炸。现在如果我 运行 以下使用标准 1.1.0.0 的程序:

{-# OPTIONS_GHC -fno-cse #-}
{-# LANGUAGE BangPatterns #-}
module Main where
import Criterion.Main
import Data.List

num :: Int
num = 10000000

lst :: a -> [Int]
lst _ = [1,2..num]

myadd :: Int -> Int -> Int
myadd !x !y = let !result = x + y in
  result

mysum = foldl' myadd 0

main :: IO ()
main = defaultMain [
  bgroup "summation" 
    [bench "mysum" $ whnf mysum (lst ())]
  ]

然后内存消耗爆炸。然而打印 核心产量:

num
num = I# 10000000

lst
lst = \ @ a_a3UV _ -> enumFromThenTo $fEnumInt (I# 1) (I# 2) num

myadd
myadd =
  \ x_a3Cx y_a3Cy ->
    case x_a3Cx of x1_X3CC { I# ipv_s461 ->
    case y_a3Cy of y1_X3CE { I# ipv1_s464 ->
    + $fNumInt x1_X3CC y1_X3CE
    }
    }

mysum
mysum = foldl' myadd (I# 0)

main
main =
  defaultMain
    (: (bgroup
      (unpackCString# "summation"#)
      (: (bench
            (unpackCString# "mysum"#)
            (whnf mysum (enumFromThenTo $fEnumInt (I# 1) (I# 2) num)))
         ([])))
       ([]))

main
main = runMainIO main

似乎没有生成 CAF。因此为什么 使用标准的后一个程序是否会导致内存消耗激增,而前一个程序 才不是?我正在使用 GHC 版本 7.8.3

在没有 criterion 的版本中,lst () 返回的列表会延迟生成,然后在 mysum 消耗它的同时逐步进行垃圾收集,因为没有其他对该列表的引用.

对于 criterion 版本,请查看 definition of whnf:

whnf :: (a -> b) -> a -> Benchmarkable
whnf = pureFunc id
{-# INLINE whnf #-}

pureFunc:

pureFunc :: (b -> c) -> (a -> b) -> a -> Benchmarkable
pureFunc reduce f0 x0 = Benchmarkable $ go f0 x0
  where go f x n
          | n <= 0    = return ()
          | otherwise = evaluate (reduce (f x)) >> go f x (n-1)
{-# INLINE pureFunc #-}

上面 go 中的 x 似乎最终会绑定到您的 lst () 返回的列表,而 n 是基准测试的迭代次数.当第一个基准测试迭代完成时,x 将全部被评估,但这次它不能被垃圾收集:它仍然保留在内存中,因为它与 shared通过递归 go f x (n-1).

进行迭代

您无需检查标准的来源就知道 lst () 将被共享:在立即计算主体的过程中,任何子表达式都将被共享(因此最多计算一次)周围的拉姆达。可能会通过重载、各种语法糖构造和编译器优化引入额外的 lambda,但是 none 正如您从核心中看到的那样,这里发生了这种情况。

如果您不希望 lst () 被共享,那么您应该将 whnf 的参数重构为类似 whnf (mysum . lst) ().

的参数