【问题标题】:Efficient hash map container in Haskell?Haskell 中高效的哈希映射容器?
【发布时间】:2013-02-01 03:38:13
【问题描述】:

我想使用 Haskell 计算存储在文件中的唯一块。 该块只是长度为 512 的连续字节,目标文件的大小至少为 1GB。

这是我最初的尝试。

import           Control.Monad
import qualified Data.ByteString.Lazy as LB
import           Data.Foldable
import           Data.HashMap
import           Data.Int
import qualified Data.List            as DL
import           System.Environment

type DummyDedupe = Map LB.ByteString Int64

toBlocks :: Int64 -> LB.ByteString -> [LB.ByteString]
toBlocks n bs | LB.null bs = []
              | otherwise = let (block, rest) = LB.splitAt n bs
                            in block : toBlocks n rest

dedupeBlocks :: [LB.ByteString] -> DummyDedupe -> DummyDedupe
dedupeBlocks = flip $ DL.foldl' (\acc block -> insertWith (+) block 1 $! acc)

dedupeFile :: FilePath -> DummyDedupe -> IO DummyDedupe
dedupeFile fp dd = LB.readFile fp >>= return . (`dedupeBlocks` dd) . toBlocks 512

main :: IO ()
main = do
  dd <- getArgs >>= (`dedupeFile` empty) . head
  putStrLn . show . (*512) . size $ dd
  putStrLn . show . (*512) . foldl' (+) 0 $ dd

它可以工作,但我对它的执行时间和内存使用感到沮丧。特别是当我与下面列出的 C++ 甚至 Python 实现相比时,它的速度要慢 3~5 倍,并且消耗的内存空间要多 2~3 倍。

import os
import os.path
import sys

def dedupeFile(dd, fp):
    fd = os.open(fp, os.O_RDONLY)
    for block in iter(lambda : os.read(fd, 512), ''):
        dd.setdefault(block, 0)
        dd[block] = dd[block] + 1
    os.close(fd)
    return dd

dd = {}
dedupeFile(dd, sys.argv[1])

print(len(dd) * 512)
print(sum(dd.values()) * 512)

我以为主要是hashmap的实现,尝试了hashmaphashtablesunordered-containers等其他实现。 但没有任何明显的区别。

请帮助我改进这个程序。

【问题讨论】:

    标签: haskell hashmap hashtable unordered-map


    【解决方案1】:

    我认为您无法击败 python 字典的性能。它们实际上是在 c 中实现的,并且经过了多年的优化,另一方面,hashmap 是新的并且没有那么优化。因此,在我看来,获得 3 倍的性能就足够了。您可以在某些地方优化您的 haskell 代码,但这仍然无关紧要。如果您仍然坚持提高性能,我认为您应该在代码中使用高度优化的 c 库和 ffi。

    这里有一些类似的讨论

    haskell beginners

    【讨论】:

    • 其实我最关心的是内存使用,我无法理解 Haskell hashmaps 内存使用过多。例如。当输入文件仅包含 600MB 的唯一数据时,它会占用大约 1GB 或更多的内存。无论如何,感谢您的回答和文章链接。我应该考虑使用 FFI。
    • @comatose,这只是 GHC。 GHC 垃圾收集策略使用了复制收集器,速度非常快,但内存开销是 2 倍。
    【解决方案2】:

    根据您的使用情况,这可能完全无关紧要,但我有点担心insertWith (+) block 1。如果您的计数达到很高的数字,您将在哈希映射的单元格中累积 thunk。使用($!) 并不重要,这只会迫使脊椎——这些值可能仍然是惰性的。

    Data.HashMap 没有像Data.Map 那样提供严格的版本insertWith'。但是你可以实现它:

    insertWith' :: (Hashable k, Ord k) => (a -> a -> a) -> k -> a 
                                       -> HashMap k a -> HashMap k a
    insertWith' f k v m = maybe id seq maybeval m'
        where
        (maybeval, m') = insertLookupWithKey (const f) k v m
    

    此外,您可能希望输出(但不输入)来自toBlocksstrict ByteStrings 列表,这将使散列更快。

    这就是我所拥有的——不过,我不是表演大师。

    【讨论】:

    • 我可以通过创建一个 data Blk = Blk {-# UNPACK #-} Word64 ... 来保存 512 个字节来挤出一点。如果您切换到严格的 ByteStrings,性能会显着提高,但我不确定其中有多少是由于缓存等影响,有多少是由于我的懒惰 ByteString 块没有合理对齐(这担心我,因为它会导致分支、复制等)。最终,unordered-containers 做得最好(4.8 sec py vs 6.5 sec hs,但这是严格的字节串),而hashtable 只是因为没有insertWith 操作而令人沮丧。
    • @luqui 感谢您的回答,我从您那里学到了一些东西。实际上,unordered-containers 中有 Data.HashMap.Strict,我试过了,但它不能让情况变得更好,严格的 ByteString 也没有。 toStrict 有点贵。
    • 如果您将defaultChunkSize 更改为512 的倍数然后重新编译/安装字节串,toStrict 可能会有好处。如果不这样做,toStrict 函数将最终在几乎每个块边界处复制数据。
    猜你喜欢
    • 1970-01-01
    • 2017-02-20
    • 1970-01-01
    • 2016-12-28
    • 1970-01-01
    • 2016-08-04
    • 2012-01-31
    • 2011-08-11
    • 2018-10-13
    相关资源
    最近更新 更多