【问题标题】:Updating a Big State Fast in Haskell在 Haskell 中快速更新大状态
【发布时间】:2011-05-15 00:27:54
【问题描述】:

对于我在 Haskell 中的矢量图形库,我必须携带一个相当大的状态:线条描边参数、颜色、剪辑路径等。我知道这样做的两种方法。引用 Haskell-cafe 的评论:“我建议你要么使用状态可变的 reader monad,要么使用状态不可变的 state monad”。

这是我的问题:更新一个大的不可变状态会破坏性能。使用大量 STRef 就像在 Haskell 中编写 C 一样:冗长而丑陋。

这里是不可变状态:

data GfxState = GfxState {
  lineWidth :: Double,
  lineCap :: Int,
  color :: Color,
  clip :: Path,
  ...
}

setLineWidth :: Double -> State GfxState ()
setLineWidth x = modify (\state -> state { lineWidth = x })

据我所知,“state { lineWidth = x }”创建了一个新的 GfxState 并让旧的 GfxState 被垃圾回收。当状态很大并且经常更新时,这会降低性能。

这里是可变状态:

data GfxState s = GfxState {
  lineWidth :: STRef s Double,
  lineCap   :: STRef s Int,
  color     :: STRef s Color,
  clip      :: STRef s Path,
  ...
  many more STRefs
}

setLineWidth :: GfxState s -> Double -> ST s ()
setLineWidth state x = writeSTRef (lineWidth state) x

现在我到处都得到 (GfxState s) 和 (ST s) 和 (STRef s),这很冗长、令人困惑,并且违背了编写简短而富有表现力的代码的精神。我可以使用 C + FFI 来读取和更新大状态,但由于我经常遇到这种模式,我希望有更好的方法。

【问题讨论】:

  • 做你正在做的事情就像在haskell中编写C语言一样,因为我看到你暗示的库接口是一个非常命令式的接口。 setLineWidth?使界面更具功能性的风格也会使实现更具功能性。
  • 对于第一个版本,用“state { lineWidth = x }”更新状态应该与旧状态共享,所以我不希望它创建一个全新的状态。您可能希望至少使状态的“原子”元素变得严格(例如 lineWidth 变为 !Double 并且 lineCap 变为 !Int),我怀疑这可能会严重影响性能。
  • @stephen, 与旧记录共享。但是如果你有一个包含 100 个字段的记录,那么每次记录更新都会复制 100 个指针。
  • @luqui - 确实,但这强烈建议“不要用 100 个字段创建记录……”并添加一些嵌套以将相关元素组合在一起。无论如何,从可理解性的角度来看,这将更加模块化和更好。
  • @luqui,setLineWidth 不属于接口。该接口有一个函数,它接受命令列表(moveTo、lineTo、setColor、setLineWidth、stroke 等)并生成图形对象列表(Stroke {path::Path、color::Color 等})。跨度>

标签: data-structures haskell state-monad


【解决方案1】:

首先我要问的是,您是否只是声称它会很慢,或者您是否进行了分析或至少注意到了性能问题?否则猜测或做出假设并不是特别有用。无论如何,我建议对您的数据进行分组,目前看起来您只是在完全平坦地布置您的结构,而您可以将相关数据(例如与行相关的数据)分组到记录中。

您可能还想分离出真正需要处于状态 monad 中的位和其他不需要进入 reader/writer monad 的位,并使用 monad 转换器将它们组合起来。关于代码的优雅,我建议使用(一等/高阶)记录库,如 fclabels。

我在一些小项目中大量使用了状态 monad(在一堆 monad 转换器中),但我还没有注意到任何性能问题。

最后,您可以使用 modify 代替 get/put 对。

【讨论】:

  • 谢谢。 fclabels 库看起来很有趣。
  • 我将示例代码更改为使用“修改”而不是“获取”和“放置”。谢谢。
【解决方案2】:

即使您的记录中有很多字段,“创建一个新字段”也只是意味着复制指针。而“让旧的垃圾收集器”只是意味着以 GHC 的分代垃圾收集器处理速度非常快的方式为每个指针释放几个字节。这一切都归结为一些机器指令。因此,即使对于图形应用程序,这也完全不会影响您的性能。

如果您确定它确实会影响性能,请将这些字段组织成一棵树。您可以使用嵌套的data 类型创建固定形状的树,甚至只使用Data.IntMap。这将使您平均获得log n / 2 指针副本。如果您知道某些字段被更频繁地访问,您可以做得更好。

这将是一个非常罕见的应用程序,它的状态如此复杂,性能要求如此之高,以至于唯一的选择是STRef 字段。但很高兴知道有这个选项。

【讨论】:

  • 谢谢,我想这就是我要做的。坚持不可变状态(即没有 ST monad),稍后在性能优化期间将字段移动到树形。我有点失望,因为这是自从切换到 Haskell 以来我第一次真正怀念 C。
【解决方案3】:

顺便说一句,如果您担心性能,您当然应该通过拆箱来改进您的数据类型表示:

data GfxState = GfxState {
  lineWidth :: {-# UNPACK #-}!Double,
  lineCap   :: {-# UNPACK #-}!Int,
  color     :: {-# UNPACK #-}!Color,
  clip      :: Path,
  ...
}

通过解包构造函数,您可以提高数据的密度,从这样的堆结构开始:

到更密集,更严格:

现在所有原子类型都布置在连续的内存槽中。更新这种类型会快很多! 顺便说一句,461.. 是 pi 字段的 Word 表示,是我的查看器库中的一个错误

您还可以减少空间泄漏的机会。

传递这种结构的成本将非常便宜,因为组件将存储在寄存器中。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-02-27
    • 1970-01-01
    • 2021-07-18
    • 2013-06-28
    • 1970-01-01
    • 1970-01-01
    • 2021-08-03
    相关资源
    最近更新 更多