【问题标题】:Is there an object-identity-based, thread-safe memoization library somewhere?某处是否有基于对象身份的、线程安全的记忆库?
【发布时间】:2011-10-16 05:33:12
【问题描述】:

我知道在堆栈溢出的haskell标签上,记忆化似乎是一个常年的话题,但我认为这个问题以前没有被问过。

我知道 Haskell 有几个不同的“现成”记忆库:

  • memo-combinators 和 memotrie 包,它们利用了一个漂亮的技巧,包括惰性无限数据结构,以纯粹的功能方式实现 memoization。 (据我了解,前者稍微灵活一些,而后者在简单情况下更易于使用:请参阅this SO answer 进行讨论。)
  • uglymemo 包,它在内部使用 unsafePerformIO,但仍然提供一个引用透明的接口。内部使用 unsafePerformIO 导致better performance 比前两个包。 (现成的,它的实现使用基于比较的搜索数据结构,而不是可能稍微更有效的哈希函数;但我认为如果你找到并替换 CmpHashableData.Map 为 @987654330 @ 并添加适当的imports,您将获得基于哈希的版本。)

但是,我不知道有任何库根据对象标识而不是对象值查找答案。这可能很重要,因为有时用作备忘录表键的对象类型(即,作为正在记忆的函数的输入)可能很大——大到完全检查对象以确定您是否'''ve seen it before it is itself a slow operation. 以前见过它本身就是一个缓慢的操作。如果您将一次又一次地将 memoized 函数应用于存储在给定“内存中的位置”1 的对象,那么速度很慢,而且也是不必要的。 (这可能会发生,例如,如果我们正在记忆一个函数,该函数在一些具有大量结构共享的大型数据结构上被递归调用。)如果我们之前已经在那个确切的对象上计算了我们的记忆函数,我们可以已经知道答案,即使不看对象本身!

实现这样一个记忆库涉及几个微妙的问题,并且正确地执行它需要语言的几个特殊支持。幸运的是,GHC 提供了我们需要的所有特殊功能,并且有一个由 Peyton-Jones、Marlow 和 Elliott 撰写的paper,它基本上为您担心大多数这些问题,解释了如何构建一个可靠的实现。他们没有提供所有细节,但他们很接近。

我可以看到可能应该担心但他们担心的一个细节是线程安全——他们的代码显然根本不是线程安全的。

我的问题是:有谁知道一个打包的库,它执行 Peyton-Jones、Marlow 和 Elliott 论文中讨论的那种记忆,填写所有细节(最好也填写适当的线程安全)?

如果做不到这一点,我想我将不得不自己编写代码:有没有人对 other 微妙之处有任何想法(除了线程安全和论文中讨论的那些),这样的实现者图书馆会很好记住吗?


更新

根据下面@luqui 的建议,这里有一些关于我所面临的确切问题的数据。假设有一个类型:

data Node = Node [Node] [Annotation]

这种类型可以用来表示内存中一种简单的有根DAG,其中Nodes是DAG节点,根只是一个区分的Node,每个节点都标注了一些Annotations内部结构,我认为,不需要我们关心(但如果它很重要,请询问,我会更具体。)如果以这种方式使用,请注意Nodes 之间可能存在显着的结构共享内存- --从根到节点的路径可能比节点本身多得多。我从一个必须与之交互的外部库中获得了这种形式的数据结构;我无法更改数据类型。

我有一个功能

myTransform : Node -> Node

其中的细节不需要我们关心(或者至少我认为如此;但如果需要,我可以再具体一点)。它将节点映射到节点,检查给定节点的注释及其直接子节点的注释,以提出具有相同子节点但可能具有不同注释的新Node。我想写一个函数

recursiveTransform : Node -> Node

其输出与数据结构“看起来相同”,就像您通过这样做得到的那样:

recursiveTransform Node originalChildren annotations = 
   myTransform Node recursivelyTransformedChildren annotations
   where
     recursivelyTransformedChildren = map recursiveTransform originalChildren    

除了它以明显的方式使用结构共享,因此它不会返回一个指数数据结构,而是一个与其输入大小相同的数据结构。

我很感激如果说 Nodes 在我得到它们之前已经编号,或者我可以更改 Node 的定义,这一切都会更容易。我不能(轻易)做这两件事。


我也对是否存在实现我提到的功能的库的一般问题感兴趣,这与我现在面临的特定具体问题完全无关:我觉得我必须在一个几次,一劳永逸地杀死龙就好了。 SPJ 等人认为值得向 GHC 添加一个而不是三个功能以支持这种形式的库的存在这一事实表明,该功能确实有用,并且不能在所有中解决> 案例。 (但我仍然非常有兴趣了解在这种特殊情况下也有帮助的解决方法:长期问题并不像我现在面临的问题那么紧迫:-))

1 从技术上讲,我并不是指内存中的位置,因为垃圾收集器有时会稍微移动对象——我真正的意思是“对象身份”。但我们可以认为这与我们对内存中位置的直观概念大致相同。

【问题讨论】:

  • @C. A. McCann:是的,在我链接到的论文中深入讨论了在实现此类库中使用 StableNames。 (事实上​​,也许那篇论文是 StableNames 的首次出现?不过我可能错了!)
  • 完全有可能。 “GHC 特性”和“将 SPJ 列为作者的论文”之间往往存在相当直接的对应关系。主要想知道,因为您没有直接提及StableName,尽管(我认为)它提供了您想要的伪指针相等原语。所以它只是你需要的记忆器本身。
  • @C. A. McCann:是的,他们甚至基本上在论文中编写了记忆器,以哈希表为模。 (并且哈希表很容易在库中找到。)我只是(a)避免输入论文中的代码,(b)避免犯愚蠢错误的可能性,因为我使事情变得线程安全。我想我不可能是第一个需要这个的人:)
  • 好的。感谢您的澄清。不,我不知道任何现有的实现,对不起!不过,很可能有一些东西可以用一个不太明显的名字来解决 Hackage。

标签: haskell memoization


【解决方案1】:

看来stable-memo 正是您所需要的(虽然我不确定它是否可以处理多个线程):

大多数 memo 组合器基于相等性进行 memoize,而 stable-memo 则基于之前是否已将完全相同的参数传递给函数(即内存中的参数相同)。

  • stable-memo 仅评估 WHNF 的密钥。

  • 这可能更适用于循环图上的递归函数。

  • stable-memo 不会保留它目前看到的密钥,如果它们不再被使用,这将允许它们被垃圾收集。如果发生这种情况,将使用终结器从备忘录表中删除相应的条目。

  • Data.StableMemo.Weak 提供了一组替代组合器,它们也避免保留函数的结果,仅在结果尚未被垃圾回收时重用。

  • 函数的参数没有类型类约束。

stable-memo 不适用于碰巧具有相同值但不是同一个堆对象的参数。这排除了许多记忆化的候选者,例如最常见的例子,其域是机器整数的朴素斐波那契实现;但是,它仍然可以用于某些领域,例如惰性自然。

【讨论】:

    【解决方案2】:

    Ekmett 刚刚上传了一个库来处理这个和更多问题(在 HacPhi 制作):http://hackage.haskell.org/package/intern。他向我保证这是线程安全的。

    编辑:实际上,严格来说,我意识到这确实有些不同。但我认为你可以将它用于你的目的。它实际上更像是一个 stringtable-atom 类型的实习库,适用于任意数据结构(包括递归数据结构)。它在内部使用 Wea​​kPtrs 来维护表。但是,它使用Ints 来索引值以避免结构相等性检查,这意味着将它们打包到数据类型中,而您想要的显然实际上是StableNames。所以我意识到这回答了一个相关的问题,但需要修改你想要避免的数据类型......

    【讨论】:

    • 太棒了!很有意思 :)。我现在只是看着它。我可能有点密集,但我不清楚如何将其用作记忆库。 (它似乎立即回答了我关于字符串实习的另一个问题!)如果我想写memoize:: (a->b)->(a->b),我该怎么做? (我也很困惑,因为它似乎没有使用StableNames,我认为这是必不可少的......)无论如何,对不起我的愚蠢!
    • 查看我上面的编辑——我刚刚意识到为什么这不符合您的要求。
    • 尽管如此,黑客的伟大壮举!实际上,我在某处有另一个关于实习字符串等的问题,其中出现了缺少适当的线程安全实习库的问题:所以也许你想把这个作为答案发布在那里,这样我就可以投票了: )。
    【解决方案3】:

    如果你想基于对象标识而不是相等来记忆,你可以使用语言中内置的现有惰性机制。

    例如,如果你有这样的数据结构

    data Foo = Foo { ... }
    expensive :: Foo -> Bar
    

    然后您可以将要记忆的值添加为一个额外的字段,让懒惰为您处理其余的事情。

    data Foo = Foo { ..., memo :: Bar }
    

    为了更容易使用,添加一个智能构造函数来打结。

    makeFoo ... = let foo = Foo { ..., memo = expensive foo } in foo
    

    虽然这不如使用库那么优雅,并且需要修改数据类型才能真正有用,但这是一种非常简单的技术,所有线程安全问题都已经为您解决了。

    【讨论】:

    • 似乎这种方法无济于事,除非人们愿意并且能够修改将用作键的类型(=您希望记忆的功能的输入)。对我来说不是这样。我错过了什么吗?
    • 虽然 if 碰巧在这种情况下,但这样更有效,节省了每次评估时的地图查找。
    • 您可以将记忆值存储在包装类型中,但您必须提升现有计算(至少需要使用记忆值的计算)才能处理包装类型,即类似于data MemoFoo = MemoFoo Foo Bar,带有makeFoo ... = let foo = Foo { ... } in MemoFoo foo (expensive foo)
    • 如果我正在尝试编写一个函数f :: Foo -> Bar,并且我(如您所建议的那样)构造一个包含两个字段的包装器类型MemoFoo,一个包含Foo 的字段和一个memo 字段包含 Bar,那么我仍然对以下内容感到困惑:f 做什么(已获得 Foo)来查找其关联的 MemoFoo 对象(如果有的话)?
    • @circular-ruin,也许您可​​以详细说明您的目标和限制条件,以便我们帮助您解决这个问题。也许有一种你没有想到的方法……
    猜你喜欢
    • 2010-11-18
    • 1970-01-01
    • 2013-01-20
    • 2013-08-13
    • 2016-12-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多