【问题标题】:Haskell space usage compile time restrictionsHaskell 空间使用编译时间限制
【发布时间】:2013-02-28 18:27:16
【问题描述】:

我很喜欢 Haskell,但空间泄漏对我来说有点担心。我通常认为 Haskell 的类型系统比 C++ 更安全,但是使用 C 风格的循环我可以相当肯定它会在不耗尽内存的情况下完成,而 Haskell 的“折叠”可能会耗尽内存,除非你小心适当的字段是严格的。

我想知道是否有一个库使用 Haskell 类型系统来确保可以编译和运行各种结构,而不会产生 thunk。例如,no_thunk_fold 会抛出编译器错误,如果使用它的方式可能会产生 thunk。我知道这可能会限制我可以做的事情,但我想要一些我可以使用的功能作为一个选项,这会让我更有信心我没有不小心在某个地方留下一个重要的非严格字段并且我会用完空间。

【问题讨论】:

  • 我不知道有什么工具可以做到这一点,但是你可以编写一个编译器插件来注释应该严格计算的函数:hackage.haskell.org/package/strict-ghc-plugin
  • 使用精细分析器。
  • 我不知道。当我刚开始学习时,空间泄漏对我来说是一个问题,但我已经建立了一种直觉,让我现在可以从很远的地方看到它们。你可以相当肯定 C 风格的循环,因为你已经建立了关于 C 中内存如何工作的直觉。这同样适用于 Haskell,但它是不同的直觉。
  • 'thunk 的建立'很好! ... 在一定程度上。自从我第一次开始学习 Haskell 以来,我没有经历过明显的空间泄漏(或者更确切地说,没有人不知道我在求爱)(当我非常擅长它们的时候!)slideshare.net/tibbe/highperformance-haskell 的前 50 页中有一些很好的指针其中讨论了各种折叠,并且相应的显式递归具有这些特性。另一个明显的点是围绕真正合理的库组织代码,例如 ByteString、Text、Vector 等。
  • 虽然不是编译时检查,但this 允许您使用运行时断言。

标签: haskell


【解决方案1】:

听起来您担心惰性求值的某些不利方面。你想确保你的折叠、循环、递归在常量内存中处理。

创建的迭代库解决了这个问题, pipes, conduit, enumerator, iteratee, iterIO.

最受欢迎和最近的是 pipesconduit。两者都超出了迭代模型。

pipes 库专注于理论上的合理性,以努力消除错误并允许设计的恒常性打开高效但高水平的抽象(我的话不是作者)。如果需要,它还提供双向流,这是迄今为止该库独有的优势。

conduit 的理论基础不如管道那么好,但它的巨大优势是目前在其上构建了更多用于解析和处理 http 流、xml 流等的相关库。查看包装页面上hackage 的导管部分。它被使用 yesod Haskell 较大且知名的 Web 框架之一。

我很喜欢使用管道库编写流媒体应用程序,特别是能够制作代理转换器堆栈。当我需要获取网页或解析一些 xml 时,我一直在使用管道库。

我还应该提到 io-streams 刚刚完成了 first official release。它的目标特别是 IO,它的名字并不奇怪,并且使用更简单的类型机制,更少的类型参数,然后 pipesconduit。主要的缺点是你被困在 IO monad 中,所以它对纯代码没有多大帮助。

{-# language NoMonoMorphismRestriction #-}                                       
import Control.Proxy

从简单的翻译开始。

map (+1) [1..10]

变成:

runProxy $ mapD (+1) <-< fromListS [1..10]

迭代者喜欢为简单的翻译提供更详细的内容,但通过更大的示例提供更大的胜利。

一个代理管道库的示例,它以常量空间生成斐波那契数

fibsP = runIdentityK $ (\a -> do respond 1                                       
                                 respond 1                                       
                                 go 1 1)                                         
  where                                                                          
    go fm2 fm1 = do  -- fm2, fm1 represents fib(n-2) and fib(n-1)                                                            
        let fn = fm2 + fm1                                                       
        respond fn -- sends fn downstream                                                              
        go fm1 fn

这些可以流式传输到标准输出 runProxy $ fibsP >-> printD -- printD 只打印下游值,代理是管道包的双向提供。

您应该查看proxy tutorialconduit tutorial,我刚刚发现它们现在在 FP Complete 的 Haskell 学校。

一种求均值的方法是:

> ((_,l),s) <- (`runStateT` 0) $ (`runStateT` 0) $ runProxy $  foldlD' ( flip $ const (+1)) <-< raiseK (foldlD' (+)) <-< fromListS [1..10::Int]
> let m = (fromIntegral . getSum) s / (fromIntegral . getSum) l
5.5

现在添加地图或过滤代理很容易。

> ((_,l),s) <- (`runStateT` 0) $ (`runStateT` 0) $ runProxy $  foldlD' ( flip $ const (+1)) <-< raiseK (foldlD' (+)) <-< filterD even <-< fromListS [1..10::Int]

编辑:重写代码以利用状态单子。

更新:

在博文beautiful folding 中演示了以可比较的方式对大量数据流进行多次计算的更多方法,然后编写直接递归。折叠转换为数据并在使用严格累加器时组合。我没有定期使用这种方法,但它似乎确实隔离了需要严格性的地方,使其更容易应用。您还应该查看answer to another question similar question,它使用 applicative 实现了相同的方法,并且可能更容易阅读,具体取决于您的偏好。

【讨论】:

  • 您提到的库似乎专注于 I/O。如果您可以提供一个纯样式示例,也许可以计算出一个平均值(这通常需要严格的元组或严格的函数,并且如果您没有正确使用,可以使用非常量空间而不会出现编译时错误)。
  • 只有 io-streams 真正关注/仅限于 io。我稍后会发布一个纯粹的例子。
  • @Clinton 发布了一些示例,尽管很难用这些简短的示例来公正地说明它们,但我鼓励您查看教程。
  • 您能发布我之前提到的“平均”(即平均值)示例吗?我发现的主要问题是由于 thunk 堆积而导致的恒定空间折叠(这对于map 来说并不是真正的问题)。
  • @Clinton 我添加了一种求均值的方法。
【解决方案2】:

Haskell 的类型系统不能做到这一点。我们可以用一个完全多态的术语来证明这一点,以吃掉任意数量的 ram。

takeArbitraryRAM :: Integer -> a -> a
takeArbitraryRAM i a = last $ go i a where
  go n x | n < 0 = [x]
  go n x | otherwise = x:go (n-1) x

做你想做的事需要substructural types。线性逻辑对应于 lambda 演算的一个高效可计算片段(尽管您还需要控制递归)。添加结构公理可以让您花费超指数时间。

Haskell 允许您伪造线性类型,以便使用索引单子来管理某些资源。不幸的是,空间和时间都融入了语言,所以你不能为他们这样做。您可以按照评论中的建议进行操作,并使用 Haskell DSL 生成具有性能限制的代码,但此 DSL 中的计算项可能会花费任意时间并使用任意空间。

不用担心空间泄漏。抓住他们。轮廓。证明复杂性界限的代码的原因。无论您使用哪种语言,您都必须这样做。

【讨论】:

  • 你能解释一下为什么上面会占用任意内存吗?我不明白。
  • 哎呀。原版没有。固定的。无论如何,想法是 tt 必须分配一个任意的长列表。
  • 不会因为last 向下移动而导致创建列表吗?由于没有其他对列表头部的引用,每个缺点将在last 移过它之后被垃圾收集,不是吗?也许您打算使用累加器...
  • @pat 优化编译器可以识别出 cons 单元只使用一次并立即释放它们,这是真的。我不认为这是 GHC 成本模型的一部分——当然,一般观点是成立的。我们可以通过一个聪明的函数来强制进行任意计算——尽管在这里我们可能需要粘贴reverse 才能实现这一点。
  • @Clinton 我的经验,共识似乎是,使用像库这样的迭代器比使用惰性列表更容易避免这种类型的内存泄漏。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-03-04
  • 1970-01-01
  • 1970-01-01
  • 2014-09-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多