【问题标题】:How to impurely modify a state associated with an object?如何不纯地修改与对象关联的状态?
【发布时间】:2015-08-26 18:12:31
【问题描述】:

在 Haskell 中,我有一个类似的容器:

data Container a = Container { length :: Int, buffer :: Unboxed.Vector (Int,a) }

这个容器是一棵扁平的树。它的访问器(!) 对向量执行二进制(log(N))搜索,以便找到存储index 的正确存储桶。

(!) :: Container a -> Int -> a
container ! index = ... binary search ...

由于连续访问可能在同一个桶中,因此可以通过以下方式进行优化:

if `index` is on the the last accessed bucket, skip the search

棘手的一点是last accessed bucket 部分。在 JavaScript 中,我只是不纯地修改了容器对象上的隐藏变量。

function read(index,object){

    var lastBucket = object.__lastBucket;

    // if the last bucket contains index, no need to search
    if (contains(object, lastBucket, index))
        var bucket = lastBucket;

    // if it doesn't
    else {
        // then we search the bucket
        var bucket = searchBucket(index,object);

        // And impurely annotate it on the container, so the
        // next time we access it we could skip the search.
        container.__lastBucket = bucket;
    }

    return object.buffer[bucket].value;
}

由于这只是一种优化,并且结果与所采用的分支无关,因此我相信它不会破坏引用透明度。在 Haskell 中,如何不纯地修改与运行时值关联的状态?

~

我想到了两种可能的解决方案。

  1. 一个全局的、可变的 hashmap 链接指向 lastBucket 值的指针,并使用 unsafePerformIO 对其进行写入。但我需要一种方法来获取对象的运行时指针,或者至少是某种唯一 id(如何?)。

  2. ContainerlastBucket :: Int 添加一个额外的字段,并在(!) 中以某种方式不纯地修改它,并将该字段视为内部字段(因为它显然破坏了引用透明度)。

【问题讨论】:

  • 对于第二种可能,你可能想用lastBucket :: IORef Int代替,用unsafePerformIO来“不纯地修改它”。您的 JS 代码无法处理 __lastBucket 可能被另一个线程的另一个 read() 调用修改的事实,因此在 if= 处具有不同的值。
  • @Xicò 当然,谢谢!不过,我希望我可以隐藏它,因为它不是 API 的一部分。还有,你说的是真的。但我这里不需要锁,对吗?只需将__lastBucket 存储在开头的变量中即可。 (JS没有线程......)。编辑:更新了 OP。
  • 如果您决定使用不安全的技巧,请注意线程。
  • 第一个使用全局地图的解决方案需要使用weakmap,否则你会遇到垃圾回收问题。
  • 将您的操作提升到ST...

标签: haskell optimization functional-programming referential-transparency


【解决方案1】:

使用解决方案(1),我设法得到了以下设计。首先,按照@Xicò 的建议,我在我的数据类型中添加了一个__lastAccessedBucket :: IORef Int 字段:

data Container a = Container { 
    length :: Int, 
    buffer :: V.Vector (Int,a), 
    __lastAccessedBucket :: IORef Int }

然后,我必须更新创建新 Container 的函数,以便使用 unsafePerformIO 创建新 IORef:

fromList :: [a] -> Container a
fromList list = unsafePerformIO $ do
    ref <- newIORef 0
    return $ Container (L.length list) buffer ref
    where buffer = V.fromList (prepare list)

最后,我创建了两个新函数,findBucketWithHint,一个纯函数,用于搜索带有猜测的索引的存储桶(即,您认为它可能在的存储桶),以及unsafeFindBucket 函数,它替换了纯findBucket 当需要性能时,始终使用最后访问的存储桶作为提示:

unsafeFindBucket :: Int -> Container a -> Int
unsafeFindBucket findIdx container = unsafePerformIO $ do 
    let lastBucketRef = __lastAccessedBucket contianer
    lastBucket       <- readIORef lastBucketRef
    let newBucket     = findBucketWithHint lastBucket findIdx container
    writeIORef lastBucketRef newBucket
    return $ newBucket

有了这个,unsafeFindBucket 在技术上是一个纯函数,具有与原始findBucket 函数相同的 API,但在某些基准测试中要快一个数量级。 我不知道这有多安全,也不知道它会在哪里导致错误。线程肯定是一个问题。

【讨论】:

  • 在这种情况下,它可能无关紧要,因为该值只是一个缓存,但通常使用atomicModifyIORef 更安全,也更惯用。
【解决方案2】:

(这更像是一个扩展评论而不是一个答案。)

首先我建议检查这是否不是premature optimization 的情况。毕竟,O(log n) 并没有那么糟糕。

如果这部分确实对性能至关重要,那么您的意图肯定是有效的。 unsafePerformIO 的常见警告是“只有在你知道自己在做什么的情况下才使用它”,你显然会这样做,它有助于同时使事情变得纯粹和快速。 请确保您遵循所有the precautions in the docs,尤其是设置正确的编译器标志(您可能想要使用the OPTIONS_GHC pragma)。

还要确保IO 操作是线程安全的。确保这一点的最简单方法是将IORefatomicModifyIORef 一起使用。

内部可变状态的缺点是,如果从多个线程访问缓存,如果它们查找不同的元素,缓存的性能会下降。

一种补救方法是显式线程化更新状态,而不是使用内部可变状态。这显然是您想要避免的,但如果您的程序使用单子,您可以添加另一个单子层,在内部为您保留状态并将查找操作公开为单子操作。

最后,您可以考虑使用splay trees 代替数组。您仍然具有(摊销)O(log n) 复杂性,但它们的最大优势在于,通过设计,它们将经常访问的元素移到顶部附近。因此,如果您要访问大小为 k 的元素子集,它们很快就会被移到顶部,因此查找操作只需 O(log k)(对于单个重复访问的元素是常量)。同样,它们会在查找时更新结构,但您可以使用与 unsafePerformIO 和原子更新 IORef 相同的方法来保持外部接口的纯净。

【讨论】:

  • Edward Kmett 一直在研究一些疯狂的向量树结构,其中包含许多不安全的东西,它们可能对此很有效,尤其是在还需要修改的情况下。
  • @viclib,你必须在 GitHub/ekmett 中四处挖掘,并在那里寻找最近的东西。我不确定实际发布了什么。
猜你喜欢
  • 1970-01-01
  • 2022-08-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-05-23
  • 2013-11-23
  • 1970-01-01
相关资源
最近更新 更多