对HashTable性能问题感到好奇

时间:2010-06-17 02:29:50

标签: haskell hashtable ghc

我读到Haskell中的哈希表存在性能问题(2006年的Haskell-Cafe和2009年的Flying Frog Consultancy's blog),因为我喜欢Haskell,所以我很担心。

那是一年前,现在的状况是什么(2010年6月)? GHC中是否修复了“哈希表问题”?

3 个答案:

答案 0 :(得分:135)

问题在于垃圾收集器需要遍历可变指针数组(“盒装数组”),寻找可能准备解除分配的数据的指针。盒装可变数组是实现哈希表的主要机制,因此特定结构显示了GC遍历问题。这在许多语言中都很常见。症状是垃圾收集过多(GC中花费的时间高达95%)。

对于可变指针数组的修复是implement "card marking" in the GC,它发生在2009年末。当你在Haskell中使用可变指针数组时,你不应该看到过多的GC。在简单的基准测试中,大散列的哈希表插入提高了10倍。

请注意,GC行走问题不会影响purely functional structures,也不会影响Haskell中的未装箱数组(如大多数数据parallel arraysvector - 类似数组。它也不会影响哈希表存储在C堆上(如judy)。这意味着它不会影响不使用命令式哈希表的日常Haskeller。

如果你在Haskell中使用哈希表,你现在不应该发现任何问题。例如,这是一个简单的散列表程序,它将1000万个int插入到散列中。我将进行基准测试,因为原始引文不提供任何代码或基准。

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 :(得分:26)

这样的问题只能通过实验来解决。但如果你没有时间或金钱做实验,你必须问别人他们的想法。当您这样做时,您可能需要考虑来源并考虑所提供的信息是否已经过任何方式的审查或审查。

Jon Harrop提出了一些关于Haskell的有趣主张。我建议您在Google Groups和其他地方搜索Harrop在Haskell,Lisp和其他函数语言方面的专业知识。您还可以阅读Chris Okasaki和Andy Gill在Haskell的Patricia树上的作品,看看他们的专业知识是如何被看到的。您还可以找到第三方已检查过哪些索赔(如果有)。然后你可以自己决定如何认真对待不同人对不同功能语言表现的主张。

哦,不要喂巨魔。


P.S。你做自己的实验是非常合理的,但也许没有必要,因为可靠的 Don Stewart在他的回答中提出了一些不错的微基准。这是Don回答的附录:


附录:在AMD Phenom 9850黑色版上使用Don Stewart的代码,时钟频率为2.5GHz,4GB RAM,32位模式,ghc -O

  • 使用默认堆,IntMap比哈希表快40%。
  • 使用2G堆,哈希表比IntMap快40%。
  • 如果我使用默认堆转到一千万个元素,IntMap比哈希表(CPU时间) 快四倍快两倍按挂钟时间。

我对这个结果感到有些惊讶,但确信功能数据结构表现相当不错。并且我相信在我们将要使用的实际条件下对您的代码进行基准测试确实是值得的。

答案 2 :(得分:6)

简而言之,即使在最新的GHC中修复,Haskell仍然无法提供具有竞争效率的字典(可变或不可变)。

Haskell的哈希表是32× slower than alternatives like C++ and .NET,GHC 6.10。这部分归因于performance bug in the GHC garbage collector that was fixed for GHC 6.12.2。但Simon Marlow的结果显示,只有5倍的性能提升仍然使Haskell的哈希表比大多数替代品慢很多倍。

纯粹的功能替代方案也比一个像样的哈希表慢得多。例如,Haskell's IntMap is 10× slower than .NET's hash table

在运行32位Windows Vista的2.0GHz E5405 Xeon上使用F#2010和the latest Haskell Platform 2010.2.0.0(昨天发布!)和GHC 6.12.3将20M int-&gt; int绑定插入到我们发现的空哈希表中Haskell实时比F#慢29倍,CPU时间慢200倍以上,因为Haskell会烧掉所有内核:

GHC 6.12.3 Data.HashTable: 42.8s (new!)
.NET hash table:            1.47s

如果您只运行短命的微基准测试,您可以禁用GHC垃圾收集器,正如Don Stewart建议的那样。通过要求托儿所生成如此之大以至于这个特定的程序永远不会填满它,他将Haskell哈希表的时间降低到这里只有1.5秒。然而,这完全破坏了托儿所生成的全部意义,并且会大大降低其他代码的性能,因为新分配的值现在在缓存中总是冷的(这就是为什么托儿所生成通常是L2缓存的大小,比这个小几个数量级。)