对 HashTable 性能问题感到好奇
Posted
技术标签:
【中文标题】对 HashTable 性能问题感到好奇【英文标题】:Curious about the HashTable performance issues 【发布时间】:2011-03-04 18:21:46 【问题描述】:我读到 Haskell 中的哈希表存在性能问题(2006 年的 Haskell-Cafe 和 2009 年的 Flying Frog Consultancy's blog),因为我喜欢 Haskell,这让我很担心。
那是一年前的事了,现在(2010 年 6 月)情况如何? GHC 中的“哈希表问题”是否已修复?
【问题讨论】:
好问题 +1 值得思考。 我的原始实验可以在这里找到,您可以轻松地为自己重现结果(您会发现 GHC 6.12 更好但仍然无法表达任何接近普通哈希表性能的解决方案,例如在 C++ 或 .NET 中):flyingfrogblog.blogspot.com/2009/04/… @Jon 在下面的 Don 的帖子中查看我的评论。在我的系统上,使用 Boost unordered_map 类的 C++ 版本仅比 Haskell 哈希表版本(使用较大的默认堆设置)快约 15%。 @BradfordLarsen 我在 2010 年 7 月再次检查,发现 F# 仍然比 Haskell 快 26 倍。 flyingfrogblog.blogspot.co.uk/2010/07/… 【参考方案1】:问题是垃圾收集器需要遍历指针的可变数组(“盒装数组”),寻找指向可能准备好释放的数据的指针。盒装的可变数组是实现哈希表的主要机制,因此特定的结构会出现 GC 遍历问题。这在许多语言中都很常见。症状是垃圾收集过多(高达 95% 的时间花在 GC 上)。
修复了implement "card marking" in the GC 的可变指针数组,这发生在 2009 年末。现在在 Haskell 中使用可变指针数组时,您应该不会看到过多的 GC。在简单的基准测试中,大哈希的哈希表插入提高了 10 倍。
请注意,GC 遍历问题不会影响purely functional structures,也不会影响未装箱的数组(就像 Haskell 中的大多数数据 parallel arrays 或类似vector 的数组一样。它也不会影响存储在 C 堆上的哈希表 (比如judy)。这意味着它不会影响不使用命令式哈希表的日常Haskellers。
如果您在 Haskell 中使用哈希表,您现在应该不会发现任何问题。例如,这里是一个简单的哈希表程序,它将 1000 万个整数插入到哈希中。我会做基准测试,因为原始引用没有提供任何代码或基准。
import Control.Monad
import qualified Data.HashTable as H
import System.Environment
main = do
[size] <- fmap (fmap read) getArgs
m <- H.new (==) H.hashInt
forM_ [1..size] $ \n -> H.insert m n n
v <- H.lookup m 100
print v
使用 GHC 6.10.2,在修复之前,插入 10M 个整数:
$ time ./A 10000000 +RTS -s
...
47s.
使用 GHC 6.13,修复后:
./A 10000000 +RTS -s
...
8s
增加默认堆区域:
./A +RTS -s -A2G
...
2.3s
避免使用哈希表并使用 IntMap:
import Control.Monad
import Data.List
import qualified Data.IntMap as I
import System.Environment
main = do
[size] <- fmap (fmap read) getArgs
let k = foldl' (\m n -> I.insert n n m) I.empty [1..size]
print $ I.lookup 100 k
我们得到:
$ time ./A 10000000 +RTS -s
./A 10000000 +RTS -s
6s
或者,或者,使用 judy 数组(它是通过外部函数接口调用 C 代码的 Haskell 包装器):
import Control.Monad
import Data.List
import System.Environment
import qualified Data.Judy as J
main = do
[size] <- fmap (fmap read) getArgs
j <- J.new :: IO (J.JudyL Int)
forM_ [1..size] $ \n -> J.insert (fromIntegral n) n j
print =<< J.lookup 100 j
运行这个,
$ time ./A 10000000 +RTS -s
...
2.1s
因此,如您所见,哈希表的 GC 问题已修复,并且总是有其他库和数据结构非常适合。总之,这不是问题。
注意:截至 2013 年,您可能应该只使用 hashtables 包,它原生支持 a range of mutable hashtables。
【讨论】:
干得好,像往常一样,全面覆盖(定义了问题,并举例说明了为什么它不再是问题)。 +1 哇,完美的答案。 Stack Overflow 社区很棒!我还不能投票(没有足够的声誉),否则我肯定会投票!非常感谢您的回答,这太可怕了! 一如既往,GHC unique, cutting-edge analysis and optimization system 的精彩演示。 出于好奇,我在自己的系统上运行了这些微基准测试,并创建了一个使用 Boost 哈希表的 C++ 版本。与运行更大堆的 Haskell 哈希表版本相比,它的 wall time 并没有快多少:C++ 1.613s,Haskell 1.862s。我很惊讶! 也许你们应该在同一台机器上运行这个 Haskell 基准测试和等效的 F# 来找出答案。【参考方案2】:这样的问题真的只能通过实验来解决。但如果你没有时间或金钱做实验,你就得问问其他人的想法。当您这样做时,您可能需要考虑来源并考虑所提供的信息是否已经过任何方式的审查或审查。
Jon Harrop 提出了一些关于 Haskell 的有趣声明。我建议您在 Google Groups 和其他地方搜索 Harrop 在 Haskell、Lisp 和其他函数式语言方面的专业知识。您还可以阅读 Chris Okasaki 和 Andy Gill 在 Haskell 的 Patricia 树上的工作,看看他们的专业知识是如何被评价的。您还可以找到谁的索赔(如果有的话)已经被第三方检查过。然后,您可以自己决定如何认真对待不同人对不同函数式语言性能的说法。
哦,不要喂巨魔。
附:你自己做实验是很合理的,但也许没有必要,因为值得信赖的 Don Stewart 在他的回答中提供了一些不错的微基准。这是唐的回答的附录:
附录:在主频为 2.5GHz、4GB RAM、32 位模式、ghc -O
的 AMD Phenom 9850 Black Edition 上使用 Don Stewart 的代码,
IntMap
比哈希表快 40%。
使用 2G 堆,哈希表比 IntMap
快 40%。
如果我使用默认堆处理一千万个元素,IntMap
比哈希表(CPU 时间)快四倍或 快两倍挂钟时间。
我对这个结果有点惊讶,但我确信函数式数据结构的性能非常好。并且在我的信念中证实,在将要使用的实际条件下对代码进行基准测试确实值得。
【讨论】:
+1 建议进行更多研究。在像 Haskell 这样基于研究的社区(例如 GHC)中,这是要走的路。 为什么在 Haskell 中使用哈希表?其目的是让 Haskell 成为一种具有良好语法、灵活性和性能的纯函数式语言。如果您正在寻找高性能的可变数据结构(即非纯函数式),那么 Haskell 可以为您工作,但也许有更好的解决方案来解决您的问题,因为该问题不是 Haskell 擅长解决的。 @Justice:我不想在 Haskell 中使用哈希表。我对默认的 Patricia 树和其他功能结构非常满意。这整个线程开始是因为 Jon Harrop 似乎正在对 Haskell 发动某种私人战争(可能是为什么这个问题现在被锁定了)。 Harrop 的策略之一是说 Haskell 中的哈希表很糟糕,纯粹的功能结构很糟糕等等。我们中的许多人不相信他的信息质量,并希望人们不要从表面上看他。这就是这个问题的意义所在。以上是关于对 HashTable 性能问题感到好奇的主要内容,如果未能解决你的问题,请参考以下文章
Django QuerySet 与原始 SQL 性能注意事项