【问题标题】:How to make a CAF not a CAF in Haskell?如何在 Haskell 中制作 CAF 而不是 CAF?
【发布时间】:2011-08-30 18:16:01
【问题描述】:

我如何将一个常量应用表单变成一个常量应用表单,而不是一个常量应用表单,以阻止它在程序的生命周期内被保留?

我试过这种方法:

-- | Dummy parameter to avoid creating a CAF
twoTrues :: () -> [[[Bool]]]
twoTrues _ = map (++ (True : repeat False)) . trueBlock <$> [1..]

但它似乎不起作用 - 配置文件显示它仍被保留并且仍将其标记为 CAF。

我找到了一个相关的 Google 结果,a reply by Simon Peyton-Jones 给 Neil Mitchell,他恰好问了这个问题 - 但不幸的是,这个答案指的是死链接。

【问题讨论】:

  • 如果有用,可以在here找到死链接所指的消息。

标签: haskell ghc compiler-optimization


【解决方案1】:

每当你使用()作为参数时,你要说的其实是

虽然我在这里声明了一个参数,但我对它是什么并不感兴趣,我不会对它的值做任何事情。

你对此并不感兴趣,因为()根本没有任何有趣的东西;你不会用它做任何事情,因为你不能用()做任何事情。

它的问题是编译器有权优化它,因为只有一个可能的值可以传递,所以它的使用总是可以预测的,那么为什么不假设呢?但它把它移回 CAF 并使这个想法行不通。

幸运的是,还有另一种方法可以做到这一点。看twoTrues下面的修改:

twoTrues :: a -> [[[Bool]]]
twoTrues _ = map (++ (True : repeat False)) . trueBlock <$> [1..]

现在你可以像这样使用twoTrues

map concat $ twoTrues()

由于a 是一个未使用的类型参数,调用者可以传递任何东西。而且因为你不知道它会是什么,所以你不知道你能用它做什么。这实际上是在迫使你忽略它的价值。所以它基本上是在声明我之前提到的相同的声明。

当然,您现在可以将任何东西(包括undefined)传递给该函数。但这并不重要,实际上正是这种可能性使这个技巧变得可行,因为编译器不再能够预测这个函数是如何被使用的。当人类用户看到这个函数时,他们应该知道你要在这里说什么,并得出结论传递 () 是最简单的,但即使他们不传递并传递其他东西,它也不会破坏任何东西,因为 Haskell 是懒惰的根本无法评估附加参数。

如果() 被用作结果呢?这更糟。由于返回() 意味着您的函数根本不做任何事情(在Haskell 中,函数的所有效果都应在其返回值中表示),编译器有权断定您的函数没有必要。

结论是,() 作为类型不应出现在类型签名中,除非与其他类型一起使用(即在 IO () 中)。

编辑

现在人们可能想知道,如果只有一种方法可以从String 实现a -&gt; String,为什么编译器不能断定它们是相同的。答案是你实际上有两种方法来实现它。

usual :: a -> String
usual _ = "Hello World!"

unusual :: a -> String
unusual a = seq a "Hello World!"

对于几乎所有输入,usual value = unusual value,但usual undefined"Hello World!"unusual undefinedundefined

从人类的角度来看,unusual 非常不寻常,因为它强制评估与最终结果无关的值。如果在任何情况下您确实需要这样的东西,只需致电seq 会更容易。此外,由于 Haskell 默认是惰性的,如果你想定义一个严格的函数,你最好记录下这个行为。因此,如果您在没有其他文档的情况下看到这样的签名,您有权假设它是以usual 方式实现的。

【讨论】:

  • 这听起来不对劲。人类会这样推理(); GHC 没有。请注意,函数也可以选择 seq 类型为 () 的值,并且可能会出现异常。
【解决方案2】:

这似乎是一个长期存在的问题http://hackage.haskell.org/trac/ghc/ticket/917。在我看来,这是一个关键的问题。

【讨论】:

  • 不过,这是一个开放的研究问题。
  • 很遗憾,我们不得不与优化器作斗争并进行反复试验(修改代码、编译、查看内核、重复直到获得所需的内核)以避免这个问题。
  • @Don Stewart 有没有关于这个问题的公开讨论?我希望看到任何建议、论点和反驳。可能很有趣。
  • @ivanvadovič 理论上,编译器有权得出结论,由于() 不包含任何内容,因此可以对其进行优化。所以我们的想法是避免将() 作为参数,因为它们总是可以被消除。相反,每当您想使用 () 作为参数时,请将其替换为未使用的类型变量。
【解决方案3】:

通过引入虚拟参数,您还必须使其看起来结果实际上取决于参数。否则,GHC 的聪明可能会将其再次变成 CAF。

我建议如下:

twoTrues u = map (++ (True : repeat False)) . trueBlock <$> [(u `seq` 1)..]

【讨论】:

    【解决方案4】:

    最简单的解决方案可能是告诉编译器内联它。 (注意:此答案未经测试。如果不起作用,请在下方评论。)

    即使(假设)编译器出于某种原因拒绝内联它,您也可以使用 cpp 代替,通过#defining 它:

    #define twoTrues (map (++ (True : repeat False)) . trueBlock <$> [1..])
    

    (虽然用这种方法,当然,它不会出现在模块的界面中,所以你只能在那个模块中使用它)。

    -cpp 选项告诉 GHC 使用 cpp 预处理输入文件。

    如果您在 n>1 处引用 twoTrues,则内联意味着复制代码 n 次。然而,虽然这在理论上是不可取的,但在实践中可能不会成为一个重大问题。

    【讨论】:

      【解决方案5】:

      泛化。如果你有一个常数值,你能把它泛化为某个变量的函数吗?我的函数在问题中的命名twoTrues 立即表明该常数是zeroTruesoneTruetwoTruesthreeTrues 等序列中的第三个 - 确实如此。因此,将twoTrues 推广到函数nTrues 接受参数n 并删除twoTrues,将从程序中消除一个CAF。

      碰巧,在这种情况下,我只为我的程序考虑了 zeroTruesoneTruetwoTrues 的情况,因为这就是我所需要的,但我的程序自然可以扩展以处理 @987654332 @ for n > 2 - 所以推广到nTrues 意味着将zeroTruesoneTrue 等的用户“一直推广”是有意义的。情况并非总是如此。

      注意:可能还有其他 CAF 需要处理,无论是在代码中,还是由 GHC 的“优化”(在这些病态情况下并不是真正的优化)产生的。

      但是,此答案可能涉及程序员的工作量超过了严格必要的工作量。正如 Don 的回答所示,实际上没有必要一概而论。

      另一方面,在某些情况下,概括一个常量可以使您更清楚您实际在做什么,并有助于可重用性。它甚至可以揭示以更好的系统方式和/或更有效地计算一系列值的方法。

      关于这种特殊情况的说明(可以忽略):在这种特殊情况下,我不想将nTrues itself 变成一个无限列表(这又是一个 CAF,重新引入原始问题!)而不是函数。一个原因是,虽然twoTrues 可以以无限列表的形式使用,但我看不出nTrues 以无限列表的形式会有什么用处(无论如何,在我的应用程序中)。

      【讨论】:

        【解决方案6】:

        一个完整的例子

        这里有一个小例子来说明这种情况:

        module A where
        
        big :: () -> [Int]
        big _ = [1..10^7]
        

        看起来像一个函数,对吧?但是 GHC 是做什么的呢?它将枚举浮动到顶层!

        A.big1 :: [Int]
        [ Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=0, Value=False,
                 ConLike=False, Cheap=False, Expandable=False,
                 Guidance=IF_ARGS [] 7 0}]
        A.big1 =
          case A.$wf1 10 A.big2 of ww_sDD { __DEFAULT ->
          eftInt 1 ww_sDD
          }
        
        A.big :: () -> [Int]
        [Arity=1,    
         Unf=Unf{Src=InlineStable, TopLvl=True, Arity=1, Value=True,
                 ConLike=True, Cheap=True, Expandable=True,
                 Guidance=ALWAYS_IF(unsat_ok=True,boring_ok=True)
                 Tmpl= \ _ -> A.big1}]
        A.big = \ _ -> A.big1
        

        哎呀!


        那么我们能做些什么呢?

        关闭优化

        这可行,-Onot,但不可取:

        A.big :: () -> [Int]
        [GblId, Arity=1]
        A.big =
          \ _ ->
            enumFromTo
              @ Int
              $fEnumInt
              (I# 1)
              (^
                 @ Int
                 @ Type.Integer
                 $fNumInt
                 $fIntegralInteger
                 (I# 10)
                 (smallInteger 7))
        

        不要内联,还有更多函数

        一切变成一个函数,包括enumFromTo,将参数传递给工作人员:

        big :: () -> [Int]
        big u = myEnumFromTo u 1 (10^7)
        {-# NOINLINE big #-}
        
        myEnumFromTo :: () -> Int -> Int -> [Int]
        myEnumFromTo _ n m = enumFromTo n m
        {-# NOINLINE myEnumFromTo #-}
        

        现在我们终于摆脱了 CAF!即使-O2

        A.myEnumFromTo [InlPrag=NOINLINE]
          :: () -> Int -> Int -> [Int]
        A.myEnumFromTo =
          \ _ (n_afx :: Int) (m_afy :: Int) ->
            $fEnumInt_$cenumFromTo n_afx m_afy
        
        A.big [InlPrag=NOINLINE] :: () -> [Int]
        A.big = \ (u_abx :: ()) -> A.myEnumFromTo u_abx A.$s^2 lvl3_rEe
        

        是的。


        什么不起作用?

        关闭 -ffull-laziness

        完整的惰性转换将定义向外浮动。 -O1 或更高版本默认开启。让我们尝试使用-fno-full-laziness 将其关闭。但是,它不起作用。

        【讨论】:

        • 我应该如何使用 big ()?如果我写main = do { print $ length $ big (); print $ length $ big () },对表达式big ()应用公共子表达式消除并浮动到顶层(根据GHC 7.0.3产生的核心),程序需要大量空间,无效为避免分享您的答案所做的全部努力。
        【解决方案7】:

        您需要对优化器隐藏 rhs 是 CAF 的事实。 应该这样做。

        twoTrues :: () -> [[[Bool]]]
        twoTrues u = map (++ (True : repeat (false u))) . trueBlock <$> [1..]
        
        {-# NOINLINE false #-}
        false :: () -> Bool
        false _ = False
        

        【讨论】:

        • 我认为[1..]无论如何都会浮到顶层。
        • 啊,是的。你也需要隐藏那个。应该做一些更模糊的代码。 :)
        最近更新 更多