列表理解中的表达式是否被冗余地评估?

时间:2013-12-01 06:42:08

标签: haskell optimization functional-programming list-comprehension ghc

let strings = ["foo", "bar", "baz"]
in  [filter (== char) (concat strings) | char <- "oa"]

GHC会在concat strings时评估char == 'o'吗?char == 'a'时评估concat strings == "foobarbaz"吗?或者它是否记得{{1}}供以后使用?

我意识到我可以通过重构此代码来避免这种不确定性,但我对代码的编译方式感兴趣。

3 个答案:

答案 0 :(得分:4)

GHC的评估可能远远少于9次。实际上它是这样做的,我们可以证明使用Debug.Trace.trace

module Main (main) where 
import Debug.Trace

x = let strings = ["foo", "bar", "baz"]
    in  [filter (== char) (trace "\nconcat strings\n" (concat strings)) | char <- "oaxyz"]

main = do
    print x

这里为-O0评估“oaxyz”5次,为-O1和-O2评估一次:

! 529)-> touch LC.hs ; ghc -O0 -o lc0 LC.hs 
[1 of 1] Compiling Main             ( LC.hs, LC.o )
Linking lc0 ...

(! 530)-> touch LC.hs ; ghc -O1 -o lc1 LC.hs 
[1 of 1] Compiling Main             ( LC.hs, LC.o )
Linking lc1 ...

(! 531)-> ./lc0; ./lc1; ./lc2

concat strings


concat strings


concat strings


concat strings


concat strings

["oo","aa","","","z"]

concat strings

["oo","aa","","","z"]

concat strings

["oo","aa","","","z"]

答案 1 :(得分:2)

克里斯在回答中说的都是真的。虽然您不能完全依赖要共享的表达式,但它是GHC共享它的有效优化,并且通常在启用优化时执行此操作。然而,你可能不想依赖这个功能,如果你可以通过解除lambda的concat调用来明确预期的共享,那我就是这样。

Debug.Trace.trace用于此类目的是了解事物评估的时间和频率的好方法。

另一种选择是查看生成的核心代码。对于这个计划:

main = print x

x = let strings = ["foo", "bar", "baz"]
    in  [filter (== char) (concat strings) | char <- "oa"]

让我们编译无需优化并查看生成的代码:

$ ghc NrEval -fforce-recomp -ddump-simpl -dsuppress-all
[1 of 1] Compiling Main             ( NrEval.hs, NrEval.o )

==================== Tidy Core ====================
Result size of Tidy Core = {terms: 50, types: 54, coercions: 0}

main
main =
  print
    ($fShow[] ($fShow[] $fShowChar))
    (let {
       a_ssN
       a_ssN = unpackCString# "foo" } in
     let {
       a1_ssQ
       a1_ssQ = unpackCString# "bar" } in
     let {
       a2_ssT
       a2_ssT = unpackCString# "baz" } in
     let {
       a3_ssU
       a3_ssU = : a2_ssT ([]) } in
     let {
       a4_ssV
       a4_ssV = : a1_ssQ a3_ssU } in
     let {
       strings_ahk
       strings_ahk = : a_ssN a4_ssV } in
     letrec {
       ds_dsE
       ds_dsE =
         \ ds1_dsF ->
           case ds1_dsF of _ {
             [] -> [];
             : ds3_dsG ds4_dsH ->
               : (filter
                    (\ ds5_dsI -> == $fEqChar ds5_dsI ds3_dsG) (concat strings_ahk))
                 (ds_dsE ds4_dsH)
           }; } in
     ds_dsE (unpackCString# "oa"))

main
main = runMainIO main

我们可以看到,即使没有优化,列表理解也被map ds_dsE中的concat strings_ahk的(内联)应用程序所取代。但是,ds1_dsF仍然在lambda(ds_dsE)下,这意味着每次评估函数时都会对它进行评估,这是两次:一次调用"oa"到字符串{ {1}},一次在递归调用中ds_dsE ds4_dsH

现在让我们将结果与优化进行比较:

$ ghc NrEval -fforce-recomp -ddump-simpl -dsuppress-all -O
[1 of 1] Compiling Main             ( NrEval.hs, NrEval.o )

==================== Tidy Core ====================
Result size of Tidy Core = {terms: 89, types: 105, coercions: 9}

main9
main9 = unpackCString# "foo"

main8
main8 = unpackCString# "bar"

main7
main7 = unpackCString# "baz"

main6
main6 = : main7 ([])

main5
main5 = : main8 main6

main_strings
main_strings = : main9 main5

main4
main4 =
  \ ds_dsT ds1_dsS ->
    : (letrec {
         go_ato
         go_ato =
           \ ds2_atp ->
             case ds2_atp of _ {
               [] -> [];
               : y_atu ys_atv ->
                 let {
                   z_XtU
                   z_XtU = go_ato ys_atv } in
                 letrec {
                   go1_XtX
                   go1_XtX =
                     \ ds3_XtZ ->
                       case ds3_XtZ of _ {
                         [] -> z_XtU;
                         : y1_Xu6 ys1_Xu8 ->
                           case y1_Xu6 of wild2_atz { C# c1_atB ->
                           case ds_dsT of _ { C# c2_atF ->
                           case eqChar# c1_atB c2_atF of _ {
                             False -> go1_XtX ys1_Xu8;
                             True -> : wild2_atz (go1_XtX ys1_Xu8)
                           }
                           }
                           }
                       }; } in
                  go1_XtX y_atu
              }; } in
       go_ato main_strings)
      ds1_dsS

main3
main3 = unpackFoldrCString# "oa" main4 ([])

main2
main2 = showList__ $fShowChar_$cshowList main3 ([])

main1
main1 = \ eta_B1 -> hPutStr2 stdout main2 True eta_B1

main
main = main1 `cast` ...

main10
main10 = \ eta_Xp -> runMainIO1 (main1 `cast` ...) eta_Xp

main
main = main10 `cast` ...

在这里,我们可以看到发生了很多事情,但特别是,对concat strings的调用已被提升到顶级并在编译时完全展开,导致main_strings指向串联的三元素字符串列表。使用go_ato调用main_strings时,显然会分享这一点。

答案 2 :(得分:1)

它将被评估两次。 GHC没有memoize。 list comprehensions desugar喜欢这个

 [term | x <- xs, y <- ys ...] -- I ignore guards

 do
  x <- xs
  y <- ys
  ...
  return term

相同
 flip concatMap xs $ \x -> flip concatMap ys $ \y -> ... [term]

很明显,term将被计算l1 * l2 * l3 ... lnlii列表的长度。