【问题标题】:Why aren't the earlier terms here being garbage-collected?为什么这里的早期术语不被垃圾收集?
【发布时间】:2015-05-05 03:05:40
【问题描述】:

如果我将Kolakoski Sequence 定义为

kolakoski :: () -> [Int]
kolakoski () = 1 : 2 : helper ()
  where
    helper () = 2 : concat (zipWith replicate (helper ()) (cycle [1, 2]))

并找到第 500,000,000 项

kolakoski () !! 500000000

我发现当使用 ghc -O 编译时,这会很快消耗大量内存。但是在关闭优化的情况下,它几乎没有使用任何东西。哪个优化导致了这个问题,我该如何关闭它?

【问题讨论】:

  • 通过添加一个虚拟参数使kolakoski 成为一个函数很聪明......但GHC 更聪明。 ;-)
  • 更具建设性的评论:另见a good way to avoid sharinghow to make a CAF not a CAF。如果您认为其中一个可能足够接近以标记为重复项,请告诉我们。
  • @DanielWagner:我应该真的不时刷新页面。我想在您发表第二条评论之前大约一分钟,我已经开始写我的答案了。顺便说一句,对我来说似乎是重复的。

标签: haskell memory garbage-collection ghc compiler-optimization


【解决方案1】:

让我们比较实际数字。如果在没有优化的情况下运行,您的 kolakoski 版本将使用大约 70k:

$ ghc --make Kolakoski-Unit && ./Kolakoski-Unit +RTS -s 2 在堆中分配了 288,002,359,096 字节 GC 期间复制了 1,343,933,816 字节 67,576 字节最大驻留(422 个样本) 52,128 字节最大斜率 2 MB 总内存在使用(0 MB 由于碎片丢失) 总时间(经过) 平均暂停 最大暂停 Gen 0 551615 colls, 0 par 1.89s 2.30s 0.0000s 0.0001s Gen 1 422 colls,0 par 0.02s 0.02s 0.0001s 0.0001s 初始化时间 0.00s(经过 0.00s) MUT时间37.34s(经过37.25s) GC时间1.91s(经过2.33s) 退出时间 0.00s(经过 0.00s) 总时间 39.25s(经过 39.58s) %GC 时间 4.9%(经过 5.9%) 分配速率 7,712,197,063 字节/MUT 秒 生产力占总用户的 95.1%,占总使用时间的 94.3%

经过优化,它使用约 4GB(尽管任务管理器中的实际内存使用量高达约 6GB)。

$ ghc --make Kolakoski-Unit -O && ./Kolakoski-Unit +RTS -s 2 在堆中分配了 64,000,066,608 字节 GC 期间复制了 27,971,527,816 个字节 3,899,031,480 字节最大驻留(34 个样本) 91,679,728 字节最大斜率 9549 MB 总内存在使用(0 MB 由于碎片丢失) 总时间(经过) 平均暂停 最大暂停 Gen 0 122806 colls, 0 par 8.67s 9.48s 0.0001s 0.0148s Gen 1 34 colls, 0 par 11.55s 69.78s 2.0524s 56.2970s 初始化时间 0.00s(经过 0.00s) MUT时间8.80s(经过8.43s) GC 时间 20.22s(经过 79.26s) 退出时间 0.03s(经过 0.89s) 总时间 29.05s(经过 88.58s) %GC 时间 69.6%(经过 89.5%) 分配速率 7,275,318,406 字节/MUT 秒 生产力占总用户的 30.4%,占总使用时间的 10.0%

如果我们使用基于列表的版本并且没有优化,内存消耗与启用优化的版本非常相似:

kolakoskiList :: [Int]
kolakoskiList = 1 : 2 : helper 
  where
    helper = 2 : concat (zipWith replicate helper (cycle [1, 2]))
$ ghc --make Kolakoski-List && ./Kolakoski-List +RTS -s 2 在堆中分配了 96,000,143,328 字节 GC 期间复制了 26,615,974,536 个字节 3,565,429,808 字节最大驻留(34 个样本) 83,610,688 字节最大斜率 8732 MB 总内存在使用(0 MB 由于碎片丢失) 总时间(经过) 平均暂停 最大暂停 Gen 0 184252 colls, 0 par 9.98s 10.16s 0.0001s 0.0006s Gen 1 34 colls, 0 par 10.45s 21.61s 0.6357s 12.0792s 初始化时间 0.00s(经过 0.00s) MUT时间12.02s(经过11.88s) GC 时间 20.44s(经过 31.77s) 退出时间 0.05s(经过 0.69s) 总时间 32.50 秒(经过 44.34 秒) %GC 时间 62.9%(经过 71.7%) 分配速率 7,989,608,807 字节/MUT 秒 生产力占总用户的 37.1%,占总使用时间的 27.2%

现在,我们可以检查GHC flag reference 中自动设置在-O 上的标志。由于列表版本似乎与优化版本做同样的事情,有人可能会认为 GHC 将 kolakoski 转换为 kolakoskiList

kolakoskiOptimized _ = kolakoskiList

让我们用-ddump-simpl -dsupress-all 在核心中检查一下:

==================== Tidy Core ====================
Result size of Tidy Core = {terms: 45, types: 30, coercions: 0}

kolakoski
kolakoski =
  \ ds_d10r ->
    case ds_d10r of _ { () ->
    : (I# 1)
      (: (I# 2)
         (letrec {
            helper_aNo
            helper_aNo =
              \ ds1_d10s ->
                case ds1_d10s of _ { () ->
                : (I# 2)
                  (concat
                     (zipWith
                        (replicate) (helper_aNo ()) (cycle (: (I# 1) (: (I# 2) ([]))))))
                }; } in
          helper_aNo ()))
    }

main
main = print $fShowInt (!! (kolakoski ()) (I# 500000000))

main
main = runMainIO main

即使您不熟悉 GHC 的核心,您也可以看到 kolakoski 与您的原始版本基本相同。现在将其与-O -ddump-simpl -dsupress-all 进行比较:

==================== Tidy Core ====================
Result size of Tidy Core = {terms: 125, types: 102, coercions: 9}

kolakoski6
kolakoski6 = I# 1

kolakoski5
kolakoski5 = I# 2

Rec {
go_r1NG
go_r1NG =
  \ ds_a14B _ys_a14C ->
    case ds_a14B of _ {
      [] -> [];
      : ipv_a14H ipv1_a14I ->
        case _ys_a14C of _ {
          [] -> [];
          : ipv2_a14O ipv3_a14P ->
            case ipv_a14H of _ { I# n#_a13J ->
            case tagToEnum# (<=# n#_a13J 0) of _ {
              False ->
                let {
                  lvl2_s1N3
                  lvl2_s1N3 = : ipv2_a14O ([]) } in
                letrec {
                  xs_a1LH
                  xs_a1LH =
                    \ m_a1LO ->
                      case tagToEnum# (<=# m_a1LO 1) of _ {
                        False -> : ipv2_a14O (xs_a1LH (-# m_a1LO 1));
                        True -> lvl2_s1N3
                      }; } in
                ++ (xs_a1LH n#_a13J) (go_r1NG ipv1_a14I ipv3_a14P);
              True -> ++ ([]) (go_r1NG ipv1_a14I ipv3_a14P)
            }
            }
        }
    }
end Rec }

lvl_r1NH
lvl_r1NH = : kolakoski5 ([])

lvl1_r1NI
lvl1_r1NI = : kolakoski6 lvl_r1NH

Rec {
xs'_r1NJ
xs'_r1NJ = ++ lvl1_r1NI xs'_r1NJ
end Rec }

Rec {
kolakoski3
kolakoski3 = : kolakoski5 kolakoski4

kolakoski4
kolakoski4 = go_r1NG kolakoski3 xs'_r1NJ
end Rec }

kolakoski2
kolakoski2 = : kolakoski5 kolakoski3

kolakoski1
kolakoski1 = : kolakoski6 kolakoski2

kolakoski
kolakoski = \ ds_d13p -> case ds_d13p of _ { () -> kolakoski1 }

此版本包含几个顶级CAFs,它们将在程序的生命周期内保留。因此,您确实会生成第 500,000,000 个值的列表并保存

现在,那里发生了什么?函数内部的东西向外浮动。让我们再次检查标志引用。 -O 暗示了一个有希望的标志:

-ffull-laziness 开启完全惰性(向外浮动绑定)。 -O暗示。

这就是导致您的问题的标志。事实上,你可以使用ghc --make -O -fno-full-laziness Kolakoski-Unit.hs 来获取你原来的内存消耗:

$ ghc --make Kolakoski-Unit.hs -O -fno-full-laziness && ./Kolakoski-Unit +RTS -s 2 在堆中分配了 192,001,417,688 字节 GC 期间复制了 637,962,464 个字节 66,104 字节最大驻留(151 个样本) 43,448 字节最大斜率 2 MB 总内存在使用(0 MB 由于碎片丢失) 总时间(经过) 平均暂停 最大暂停 Gen 0 368364 colls, 0 par 1.34s 1.32s 0.0000s 0.0002s Gen 1 151 colls,0 par 0.00s 0.01s 0.0001s 0.0003s 初始化时间 0.00s(经过 0.00s) MUT时间27.89s(经过28.13s) GC时间1.34s(经过1.33s) 退出时间 0.00s(经过 0.00s) 总时间 29.25s(经过 29.46s) %GC 时间 4.6%(经过 4.5%) 分配速率 6,884,084,443 字节/MUT 秒 生产力占总用户的 95.4%,占总使用时间的 94.7%

相关问题

【讨论】:

    猜你喜欢
    • 2010-12-19
    • 2014-08-08
    • 2011-12-05
    • 1970-01-01
    • 2010-11-10
    • 2013-04-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多