如何不纯地修改与对象关联的状态?

时间:2015-08-26 18:12:32

标签: haskell optimization functional-programming referential-transparency

在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添加一个额外字段,并以某种方式在(!)内进行不必要的修改,并将该字段视为内部字段(因为它显然会破坏参照透明度)。 / p>

2 个答案:

答案 0 :(得分:2)

使用解决方案(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,但在某些基准测试中速度要快一个数量级。 我不知道这是多么安全以及它可能导致错误的地方。线程当然是一个问题。

答案 1 :(得分:2)

(这是一个扩展的评论而非答案。)

首先,我建议检查这不是premature optimization的情况。毕竟, O(log n)表示不好。

如果这部分确实对性能至关重要,那么你的意图肯定是有效的。 unsafePerformIO的常见警告是&#34;只有当您知道自己正在做什么时才使用它#34;这显然是你做的事情,它可以帮助同时让事情变得纯粹和快速。 请确保遵循所有the precautions in the docs,特别是设置正确的编译器标志(您可能希望使用the OPTIONS_GHC pragma)。

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

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

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

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