问题是垃圾收集器需要遍历指针的可变数组(“盒装数组”),寻找指向可能准备好释放的数据的指针。盒装的可变数组是实现哈希表的主要机制,因此特定的结构会出现 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。