3

I have a memory consumption problem when using TemplateHaskell in RuzzSolver, one of my Haskell project. Sources of RuzzSolver are available on GitHub .

To achieve good performance, I load a ~380000 words dictionary into a Tree structure (from the containers package). This greatly speeds up the solving of grids but the loading itself takes some time (between 1 and 2 seconds depending on the CPU).

I would like to create the structure directly during compile-time with TemplateHaskell.

Therefore I transformed the dictionary loading:

-- Dictionary.hs, line 155
getDictionary :: String -> IO Dictionary
getDictionary dictionaryFilePath = do
    content <- readFile dictionaryFilePath
    return $ foldl (+++) [] (createTree <$> lines content)

into this function:

-- Dictionary.hs, line 164
getDictionaryQ :: String -> Q Exp
getDictionaryQ dictionaryFilePath = do
    content <- runIO $ readFile dictionaryFilePath
    lift $ foldl (+++) [] (createTree <$> lines content)

view Dictionary.hs

It allowed me to go from:

-- ruzzSolver.hs, line 68
dictionary <- getDictionary "dictionary/ruzzdictionary.txt"

to:

-- ruzzSolver.hs, line 68
let dictionary = $(getDictionaryQ "dictionary/ruzzdictionary.txt")

view ruzzSolver.hs

It (should) works but it requires too much memory to compile! On my 8 Gb PC, I had to stop GHC when it reached consumption of 12 GB. Reducing the dictionary to 38000 words lets it compile but it still needs 3 to 4 GB.

Is there a way to make GHC use less memory when compiling this TemplateHaskell code? Or another way to embed this structure into the executable?

zigazou
  • 1,725
  • 9
  • 13
  • This is not directly related to your problem, but it sounds like you're using the tree as some sort of trie. There are libraries out there that offer tries, including [bytestring-trie](https://hackage.haskell.org/package/bytestring-trie) and [word-trie](https://hackage.haskell.org/package/word-trie). – dfeuer Sep 09 '15 at 22:46
  • @dfeuer you’re right, it’s a trie :-) – zigazou Sep 10 '15 at 06:01
  • Seems like loading a couple meg file should not be a big deal. Look carefully at your loading code. Are you using (lazy) [bytestrings](https://hackage.haskell.org/package/bytestring-0.10.6.0/docs/Data-ByteString-Lazy.html) to load the file? I hope the file is already sorted? Can you come up with a file representation that will store the tree in a state that takes very little computation to load; e.g. using [Data.Binary](https://hackage.haskell.org/package/binary-0.7.6.1/docs/Data-Binary.html)? Crazy idea: implement a trie based on [mmap](https://hackage.haskell.org/package/mmap). – luqui Sep 10 '15 at 06:13
  • 1
    Obviously if you are precomputing a gigantic AST, you should have optimization turned off for that module. – luqui Sep 10 '15 at 06:15
  • @luqui yes, it’s a gigantic AST ;-) The file is already sorted. I do not use (lazy) bytestrings, I use the standard readFile. I once tried to use Text functions to load the file, but it didn’t make any difference. – zigazou Sep 10 '15 at 06:26
  • @luqui turning off optimization didn’t help. About the crazy idea: I’m not that advanced in Haskell ;-) – zigazou Sep 10 '15 at 06:28
  • @zigzou - I updated my answer with a pointer to some sample code. – ErikR Sep 10 '15 at 11:31

1 Answers1

1

Perhaps you can "embed" the trie into the executable to save on loading and creation time, but one problem I foresee is that conventional Haskell data structures are quite bloated compared to data structures in other languages.

Also, most containers allow for insertion and deletion, but it looks like your data is constant so you'll only need the final data structure. Moreover you'll only be using it for queries like:

  • Does this word exist in the dictionary?
  • And, is this string a prefix of some word in the dictionary?

You want a compact representation of the dictionary with some sort of pre-computed index to make lookups fast.

Some options:

Option 1: Create a BerkeleyDB database.

Such a database allows greater-than and less-than queries.

Pros: No database load time.

Cons: Queries require disk accesses. Although, once pages are read by the OS they should be cached and subsequent reads should be fast.

Note - I've written a boggle solver in perl using a Berkeley DB, so this approach is very viable.

Similar to a BerkeleyDB is a CDB (Constant Database) for which there is also a Haskell package. However, a CDB only supports equality queries, so it's probably not usable for your application.

Option 2. Represent the dictionary simply as the sorted file of the words. Create a custom index to make queries efficient.

A simple index could just be a 26*26*26 element array indicating the offset into the file of each three-letter prefix. Such a small index can be compiled into the program. Load the dictionary as a single (strict) ByteString.

Use the index and binary search within the byte string to resolve queries. Perhaps the ByteString functions will work well here, but as a last resort you can always use an Int offset into the loaded dictionary as a "pointer" which you can move around to find the beginning of the next word.

You might be able to compile the dictionary ByteString into the executable, but loading 4 MB of data shouldn't take too long - especially if it is already in the OS cache.

Update: An example of this second idea may be found here.

ErikR
  • 51,541
  • 9
  • 73
  • 124
  • Though it didn’t solve the TemplateHaskell memory problem, it’s been of great help, thanks ! :-) – zigazou Sep 10 '15 at 16:07