【问题标题】:Haskell vector C++ push_back analogueHaskell 向量 C++ push_back 类比
【发布时间】:2015-07-23 21:24:16
【问题描述】:

我发现 Haskell Data.Vector.* 错过了 C++ std::vector::push_back 的功能。有grow/unsafeGrow,但它们似乎有O(n)复杂度。

有没有办法在一个元素的 O(1) 摊销时间内增长向量?

【问题讨论】:

  • 也许看看Data.Sequence?大多数情况下,它都有O(1) 摊销。除了push_back,你还需要什么?
  • 这很好,但出于性能原因,我使用未装箱的向量。我需要连续存储。我也想随机访问二进制搜索。
  • Alec,我可以围绕 Vector 创建一个呈指数增长的包装器,但如果 Haskell 中没有第二个有用的 Vector 操作,我会感到惊讶。
  • vector 操作会受到融合,并且无论如何都不会执行您假设来自 c++ 的方式。您应该进行基准测试
  • @JeremyList 是的,但是由于指数增长,调用 push_back n 次也是 O(n),而不是 O(n^2)。因此,一个 push_back 的 摊销 复杂度是 O(1)。

标签: haskell vector data-structures amortized-analysis


【解决方案1】:

不,Data.Vector 中确实没有这样的设施。使用MutableArray 从头开始​​实现这一点并不难,就像Data.Vector.Mutable 所做的那样(见下面我的实现),但有一些明显的缺点。特别是,它的所有操作最终都发生在某个状态上下文中,通常是ST 或IO。这样做的缺点是

  1. 任何操作这种数据结构的代码最终都必须是单子的
  2. 编译器不太可能进行优化。例如,像vector 这样的库使用称为fusion 的非常聪明的东西来优化中间分配。这种事情在状态上下文中是不可能的。
  3. 并行性将变得更加困难:在ST 中我什至不能有两个线程,而在IO 中我将到处都有竞争条件。这里令人讨厌的一点是,任何共享都必须在IO 中进行。

似乎这一切还不够,垃圾收集在纯代码中也表现得更好。

那我该怎么办?

您并不经常需要正是这种行为 - 通常您最好使用不可变的数据结构(从而避免所有上述问题),它会做类似的事情。仅限于 GHC 附带的containers,一些替代方案包括:

  • 如果您几乎总是只使用push_back,也许您只需要一个堆栈(一个普通的旧[a])。
  • 如果您预计push_back 比查找更多,Data.Sequence 会为您提供O(1) 附加到任一端和O(log n) 查找。
  • 如果您对很多操作感兴趣,尤其是类似 hashmap 的操作,Data.IntMap 已经非常优化。即使这些操作的理论成本是O(log n),您也需要相当大的IntMap 才能开始感受这些成本。

制作类似 C++ 的东西 vector

当然,如果你不关心最初提到的限制,那么没有理由不使用类似 C++ 的向量。只是为了好玩,我从头开始实现了这个(需要包 data-default 和 primitive)。

此代码可能不在某些库中的原因是它违背了 Haskell 的大部分精神(我这样做是为了符合 C++ 样式向量)。

  • 真正创建新向量的唯一操作是newVector - 其他所有操作都会“修改”现有向量。由于pushBack 不会返回一个新的GrowVector,它必须修改现有的(包括它的长度和/或容量),所以length 和capacity 必须是“指针”。反过来,这意味着即使获取 length 也是一个单子操作。
  • 虽然没有拆箱,但复制 vectors data family approach 并不会太难 - 只是很乏味1。

话虽如此:

module GrowVector (
  GrowVector, newEmpty, size, read, write, pushBack, popBack
) where 

import Data.Primitive.Array
import Data.Primitive.MutVar
import Data.Default
import Control.Monad
import Control.Monad.Primitive (PrimState, PrimMonad)
import Prelude hiding (length, read)

data GrowVector s a = GrowVector
  { underlying :: MutVar s (MutableArray s a) -- ^ underlying array
  , length :: MutVar s Int                    -- ^ perceived length of vector
  , capacity :: MutVar s Int                  -- ^ actual capacity
  }

type GrowVectorIO = GrowVector (PrimState IO)

-- | Make a new empty vector with the given capacity. O(n)
newEmpty :: (Default a, PrimMonad m) => Int -> m (GrowVector (PrimState m) a)
newEmpty cap = do
  arr <- newArray cap def
  GrowVector <$> newMutVar arr <*> newMutVar 0 <*> newMutVar cap

-- | Read an element in the vector (unchecked). O(1)
read :: PrimMonad m => GrowVector (PrimState m) a -> Int -> m a
g `read` i = do arr <- readMutVar (underlying g); arr `readArray` i

-- | Find the size of the vector. O(1)
size :: PrimMonad m => GrowVector (PrimState m) a -> m Int
size g = readMutVar (length g)

-- | Double the vector capacity. O(n)
resize :: (Default a, PrimMonad m) => GrowVector (PrimState m) a -> m ()
resize g = do
  curCap <- readMutVar (capacity g)         -- read current capacity
  curArr <- readMutVar (underlying g)       -- read current array
  curLen <- readMutVar (length g)           -- read current length
  newArr <- newArray (2 * curCap) def       -- allocate a new array twice as big
  copyMutableArray newArr 1 curArr 1 curLen -- copy the old array over
  underlying g `writeMutVar` newArr         -- use the new array in the vector
  capacity g `modifyMutVar'` (*2)           -- update the capacity in the vector

-- | Write an element to the array (unchecked). O(1)
write :: PrimMonad m => GrowVector (PrimState m) a -> Int -> a  -> m ()
write g i x = do arr <- readMutVar (underlying g); writeArray arr i x

-- | Pop an element of the vector, mutating it (unchecked). O(1)
popBack :: PrimMonad m => GrowVector (PrimState m) a -> m a
popBack g = do
  s <- size g;
  x <- g `read` (s - 1)
  length g `modifyMutVar'` (+ negate 1)
  pure x

-- | Push an element. (Amortized) O(1)
pushBack :: (Default a, PrimMonad m) => GrowVector (PrimState m) a -> a -> m ()
pushBack g x = do
  s <- readMutVar (length g)                -- read current size
  c <- readMutVar (capacity g)              -- read current capacity
  when (s+1 == c) (resize g)                -- if need be, resize
  write g (s+1) x                           -- write to the back of the array
  length g `modifyMutVar'` (+1)             -- increase te length

grow 的当前语义

我认为github issue 很好地解释了语义:

我认为预期的语义是它可能会执行重新分配,但不能保证这样做,并且所有当前的实现都执行更简单的复制语义,因为对于堆分配,成本应该大致相同。

基本上你应该使用grow,当你想要一个更大的新可变向量时,从旧向量的元素开始(不再关心旧向量)。这非常有用——例如,可以使用MVector 和grow 实现GrowVector。


1 方法是,对于您想要拥有的每种新类型的未装箱矢量,您创建一个 data instance 将您的类型“扩展”为固定数量的未装箱数组(或其他未装箱的数组)向量)。这就是data family 的重点——允许一个类型的不同实例具有完全不同的运行时表示,并且还可以扩展(如果需要,您可以添加自己的data instance)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-06-09
    • 2014-09-13
    • 1970-01-01
    • 2013-03-28
    • 2011-05-17
    • 1970-01-01
    相关资源
    最近更新 更多