52

I read that hash tables in Haskell had performance issues (on the Haskell-Cafe in 2006 and Flying Frog Consultancy's blog in 2009), and since I like Haskell it worried me.

That was a year ago, what is the status now (June 2010)? Has the "hash table problem" been fixed in GHC?

rob mayoff
  • 375,296
  • 67
  • 796
  • 848
Alessandro Stamatto
  • 1,489
  • 2
  • 14
  • 18
  • 1
    Good question +1 for thinking to ask. – Robert Massaioli Jun 17 '10 at 04:13
  • 3
    My original experiment is available here and you can easily reproduce the results for yourself (you'll find that GHC 6.12 is better but still cannot express any solution that comes close to the performance of an ordinary hash table, e.g. in C++ or .NET): http://flyingfrogblog.blogspot.com/2009/04/f-vs-ocaml-vs-haskell-hash-table.html – J D Jun 18 '10 at 17:18
  • 5
    @Jon See my comment in Don's post below. A C++ version using the Boost unordered_map class was only about 15% faster than the Haskell hash table version (using the larger default heap settings) on my system. – Brad Larsen Jun 18 '10 at 23:48
  • 1
    @BradfordLarsen I checked this again in July 2010 and found that F# was still up to 26× faster than Haskell. http://flyingfrogblog.blogspot.co.uk/2010/07/haskells-hash-tables-revisited-part-2.html – J D Apr 08 '12 at 00:36

2 Answers2

137

The problem was that the garbage collector is required to traverse mutable arrays of pointers ("boxed arrays") looking for pointers to data that might be ready to deallocate. Boxed, mutable arrays are the main mechanism for implementing a hashtable, so that particular structure showed up the GC traversal issue. This is common to many languages. The symptom is excessive garbage collection (up to 95% of time spent in GC).

The fix was to implement "card marking" in the GC for mutable arrays of pointers, which occured in late 2009. You shouldn't see excessive GC when using mutable arrays of pointers in Haskell now. On the simple benchmarks, hashtable insertion for large hashes improved by 10x.

Note that the GC walking issue doesn't affect purely functional structures, nor unboxed arrays (like most data parallel arrays, or vector-like arrays, in Haskell. Nor does it affect hashtables stored on the C heap (like judy). Meaning that it didn't affect day-to-day Haskellers not using imperative hash tables.

If you are using hashtables in Haskell, you shouldn't observe any issue now. Here, for example, is a simple hashtable program that inserts 10 million ints into a hash. I'll do the benchmarking, since the original citation doesn't present any code or benchmarks.

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

With GHC 6.10.2, before the fix, inserting 10M ints:

$ time ./A 10000000 +RTS -s
...
47s.

With GHC 6.13, after the fix:

./A 10000000 +RTS -s 
...
8s

Increasing the default heap area:

./A +RTS -s -A2G
...
2.3s

Avoiding hashtables and using an 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

And we get:

$ time ./A 10000000 +RTS -s        
./A 10000000 +RTS -s
6s

Or, alternatively, using a judy array (which is a Haskell wrapper calling C code through the foreign-function interface):

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

Running this,

$ time ./A 10000000 +RTS -s
...
2.1s

So, as you can see, the GC issue with hashtables is fixed, and there have always been other libraries and data structures which were perfectly suitable. In summary, this is a non-issue.

Note: as of 2013, you should probably just use the hashtables package, which supports a range of mutable hashtables natively.

Don Stewart
  • 137,316
  • 36
  • 365
  • 468
  • 1
    It was written it will me merged into 6.12.2 – Maciej Piechotka Jun 17 '10 at 04:13
  • 3
    Nice work, as usual, and complete coverage (defined the problem and showed, by example, why it is no longer an issue). +1 – Robert Massaioli Jun 17 '10 at 04:18
  • 6
    Wow, perfect answer. Stack Overflow community is great! I can't upvote yet (not enough reputation ), otherwise i would sure upvote! Thanks a lot for the answer, it was formidable! – Alessandro Stamatto Jun 17 '10 at 04:21
  • 5
    As always, a lovely demonstration of GHC's [unique, cutting-edge analysis and optimization system](http://www.reddit.com/r/haskell/comments/cbnwp/when_translating_c_to_haskell_use_the_same/c0rh4sv). – C. A. McCann Jun 17 '10 at 04:32
  • As it turns out, Don Stewart is also a fine proof engine, if somewhat by proxy! – Norman Ramsey Jun 17 '10 at 20:52
  • Don, you need a faster machine! – Norman Ramsey Jun 17 '10 at 21:04
  • If an IntMap is faster than a hashtable, why ever bother with a hashtable? A hashtable is only useful when it's O(1). – Gabe Jun 18 '10 at 18:29
  • Different scaling characteristics compared to generalized tries. – Don Stewart Jun 18 '10 at 20:07
  • 11
    Out of curiosity, I ran these micro-benchmarks on my own system, and whipped up a C++ version that uses the Boost hash table. Compared to the Haskell hash table version run with larger heap, its wall time was not much faster: C++ 1.613s, Haskell 1.862s. I was surprised! – Brad Larsen Jun 18 '10 at 23:44
  • 1
    Your data is obsolete, Jon. That's what this post is about. – Don Stewart Jun 18 '10 at 23:53
  • 2
    Perhaps you guys should run this Haskell benchmark and an equivalent F# on the same machine to find out. – Jules Jun 19 '10 at 22:57
  • 1
    What are the programs and compiler options you're using? – Jules Jun 24 '10 at 12:22
  • 1
    Benchmarks show Haskell is slower; that seems incontrovertible. The real question is: does the slower hashtable performance in Haskell matter for your particular task? If not, feel free to choose Haskell, but if it does, choose something else. Performance is only one characteristic of a language implementation. How important it is depends entirely upon what you're trying to achieve with it. – Duncan Bayne Apr 07 '11 at 04:47
28

A question like this can really be settled only by experiment. But if you don't have the time or money to do experiments, you have to ask other people what they think. When you do so, you might want to consider the source and consider whether the information given has been reviewed or vetted in any way.

Jon Harrop has advanced some interesting claims about Haskell. Let me suggest that you search on Google Groups and elsewhere for evidence of Harrop's expertise in Haskell, Lisp, and other functional languages. You could also read the work by Chris Okasaki and Andy Gill on Patricia trees in Haskell, see how their expertise is regarded. You can also find whose claims, if any, have been checked by a third party. Then you can make up your own mind how seriously to take different people's claims about the performance of different functional languages.

Oh, and don't feed the troll.


P.S. It would be quite reasonable for you to do your own experiments, but perhaps not necessary, since the trusty Don Stewart presents some nice microbenchmarks in his fine answer. Here's an addendum to Don's answer:


Addendum: Using Don Stewart's code on an AMD Phenom 9850 Black Edition clocked at 2.5GHz with 4GB RAM, in 32-bit mode, with ghc -O,

  • With the default heap, the IntMap is 40% faster than the hash table.
  • With the 2G heap, the hash table is 40% faster than the IntMap.
  • If I go to ten million elements with the default heap, the IntMap is four times faster than the hash table (CPU time) or twice as fast by wall-clock time.

I'm a little surprised by this result, but reassured that functional data structures perform pretty well. And confirmed in my belief that it really pays to benchmark your code under the actual conditions in which it's going to be used.

Norman Ramsey
  • 198,648
  • 61
  • 360
  • 533
  • 2
    +1 for suggesting a bit more research. In a community like Haskell that is based on research (like GHC for example), this is the way to go. – Robert Massaioli Jun 17 '10 at 04:15
  • 2
    Why are you using hash tables in Haskell? The intent is for Haskell to be a purely functional language with good syntax, flexibility, and performance. If you're looking for a high-performance mutable data structure (i.e., non- purely functional), then Haskell can work for you, but maybe there are better solutions for your problem because that problem is not what Haskell specializes in solving. – yfeldblum Jun 19 '10 at 17:38
  • 11
    @Justice: *I* don't want to use hash tables in Haskell. I'm perfectly happy with the default Patricia trees and other functional structures. This whole thread started because Jon Harrop seems to be waging some sort of private war against Haskell (probably why this question is now locked). One of Harrop's tactics has been to say that hash tables in Haskell are terrible, purely functional structures are terrible, etc etc. A number of us don't trust the quality of his information and wish for people not to take him at face value. That's what this question is about. – Norman Ramsey Jun 20 '10 at 01:57