【问题标题】:reuse/memoization of global polymorphic (class) values in HaskellHaskell中全局多态(类)值的重用/记忆
【发布时间】:2015-07-30 12:42:32
【问题描述】:

我关心多态“全局”类值是否以及何时被共享/记忆,尤其是跨模块边界。我已阅读 thisthis,但它们似乎并不能完全反映我的情况,而且我看到的一些行为与人们对答案的预期不同。

考虑一个公开了一个计算成本很高的值的类:

{-# LANGUAGE FlexibleInstances, UndecidableInstances #-}

module A

import Debug.Trace

class Costly a where
  costly :: a

instance Num i => Costly i where
  -- an expensive (but non-recursive) computation
  costly = trace "costly!" $ (repeat 1) !! 10000000

foo :: Int
foo = costly + 1

costlyInt :: Int
costlyInt = costly

还有一个单独的模块:

module B
import A

bar :: Int
bar = costly + 2

main = do
  print foo
  print bar
  print costlyInt
  print costlyInt

运行main 会产生两个单独的costly 评估(如跟踪所示):一个用于foo,另一个用于bar。我知道costlyInt 只是从foo 返回(评估的)costly,因为如果我从main 中删除print foo,那么第一个costlyInt 变得昂贵。 (我也可以通过将foo 的类型概括为Num a => a 来使costlyInt 执行单独的评估。)

我想我知道为什么会发生这种行为:Costly 的实例实际上是一个接受 Num 字典并生成 Costly 字典的函数。因此,当编译 bar 并解析对 costly 的引用时,ghc 会生成一个新的 Costly 字典,其中包含一个昂贵的 thunk。问题 1:我对此是否正确?

有几种方法可以只对costly 进行一次评估,包括:

  • 将所有内容放在一个模块中。
  • 删除Num i 实例约束,只定义一个Costly Int 实例。

不幸的是,这些解决方案的类似物在我的程序中不可行——我有几个模块以多态形式使用类值,并且只有在顶级源文件中才最终使用具体类型。

还有一些变化不会减少评估次数,比如:

  • 在实例中的 costly 定义上使用 INLINE、INLINABLE 或 NOINLINE。 (我没想到这会奏效,但嘿,值得一试。)
  • 在实例定义中使用SPECIALIZE instance Costly Int pragma。

后者让我感到惊讶——我希望它基本上等同于上面 确实 工作的第二项。也就是说,我认为它会生成一个特殊的Costly Int 字典,所有foobarcostlyInt 都会共享它。我的问题2:我在这里错过了什么?

我的最后一个问题:是否有任何相对简单且万无一失的方法来获得我想要的东西,即跨模块共享特定具体类型的所有对 costly 的引用?从我目前看到的情况来看,我怀疑答案是否定的,但我仍然抱有希望。

【问题讨论】:

  • 这个问题和my earlier one到底有什么不同?
  • 据我所知,您问题的代价来自递归和字典传递,这会导致指数重新评估。一旦后者被固定,计算本身就会很快。但是,如果两个模块引用 fibsImplicitDict,它们会得到不同的 thunk,这些 thunk 将被单独评估。我负担不起,因为我的计算本质上是昂贵的(不是由于递归)。
  • PS —— 似乎存在带有约束的类实例这一事实在这里也发挥了作用。
  • 这里需要使用类型类吗?如果它只有一个实例,则不需要它。
  • 在我的真实程序中,是的——我有很多实例。 Costly 表示一个具有单位根的环,我有复数、整数 mod q、有限域等的实例。它们的单位根不够便宜,无法在所有地方重新计算。

标签: haskell polymorphism memoization


【解决方案1】:

在 GHC 中控制共享很棘手。 GHC 进行了许多可能影响共享的优化(例如内联、浮动等)。

在这种情况下,要回答为什么 SPECIALIZE pragma 没有达到预期效果的问题,让我们看看 B 模块的核心,特别是 bar 函数:

Rec {
bar_xs
bar_xs = : x1_r3lO bar_xs
end Rec }

bar1 = $w!! bar_xs 10000000
--     ^^^ this repeats the computation. bar_xs is just repeat 1

bar =
  case trace $fCostlyi2 bar1 of _ { I# x_aDm -> I# (+# x_aDm 2) }
  --         ^^^ this is just the "costly!" string

这并没有如我们所愿。 GHC 决定只内联costly 函数,而不是重用costly

所以我们必须防止 GHC 内联代价高昂,否则计算会重复。我们如何做到这一点?您可能认为添加 {-# NOINLINE costly #-} pragma 就足够了,但不幸的是,没有内联的专业化似乎不能很好地协同工作:

A.hs:13:3: Warning:
    Ignoring useless SPECIALISE pragma for NOINLINE function: ‘$ccostly’

但是有一个技巧可以说服 GHC 做我们想做的事:我们可以用下面的方式写costly

instance Num i => Costly i where
  -- an expensive (but non-recursive) computation
  costly = memo where
    memo :: i
    memo = trace "costly!" $ (repeat 1) !! 10000000
    {-# NOINLINE memo #-}
  {-# SPECIALIZE instance Costly Int #-}
-- (this might require -XScopedTypeVariables)

这允许我们专门化costly,同时避免我们计算的内联。

【讨论】:

  • 优秀!我曾尝试过备忘录技巧,以及 NOINLINE 与 SPECIALIZE (只是得到相同的错误),但没想到以这种方式组合它们。这个技巧适用于我的一些全局值,但对于其他一些我不能专门化,因为我只知道最顶层应用程序模块的具体类型。这个案子有希望吗?
  • @ChrisPeikert 我没检查过,但也许在这种情况下内联costly 可以达到与专业化相同的效果?
  • 对,内联与我想要的相反(即共享)。
  • @ChrisPeikert 我认为这是不可能的。如果 GHC 事先不知道函数应该被记忆的类型,就没有办法制作适当的“顶级备忘录”来存储记忆值。唯一可行的方法是,如果您使用 costly 内联所有函数,然后以某种方式安排所有备忘录由 CSE 浮出并消除。我怀疑这是否会可靠地工作......
  • 或者,让 GHC 自动专业化 costly 和 CSE 同一类型的 memo 的所有专业化。
猜你喜欢
  • 2014-09-23
  • 2020-09-19
  • 2013-05-11
  • 2020-10-25
  • 2011-03-13
  • 2016-04-11
  • 2015-02-18
  • 2013-08-20
  • 2011-11-16
相关资源
最近更新 更多