在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中,如何不可能修改与运行时值相关联的状态?
〜
我想到了两种可能的解决方案。
一个全局的,可变的hashmap,它将指针链接到lastBucket
,并使用unsafePerformIO对其进行写入。但我需要一种方法来获取对象的运行时指针,或者至少是某种类型的唯一ID(如何?)。
向Container
,lastBucket :: Int
添加一个额外字段,并以某种方式在(!)
内进行不必要的修改,并将该字段视为内部字段(因为它显然会破坏参照透明度)。 / p>
答案 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
操作是线程安全的。确保这一点的最简单方法是将IORef
与atomicModifyIORef
一起使用。
内部可变状态的缺点是,如果从多个线程访问缓存,如果它们查找不同的元素,缓存的性能将会恶化。
一种补救方法是显式线程化更新后的状态,而不是使用内部可变状态。这显然是你想要避免的,但如果你的程序使用monad,你可以添加另一个monadic层,在内部为你保持状态,并将查找操作公开为monadic动作。
最后,您可以考虑使用splay trees而不是数组。您仍然拥有(摊销) O(log n)复杂性,但它们的最大优势在于,通过设计,它们可以将频繁访问的元素移动到顶部附近。因此,如果您要访问大小为 k 的元素的子集,它们很快就会移到顶部,因此查找操作将只是 O(log k )(对于单个重复访问的元素,为常量)。同样,他们更新了查找结构,但您可以使用unsafePerformIO
的相同方法和IORef
的原子更新来保持外部接口纯净。