Here’s a simple program I wrote on a whim tonight, to take a very basic look at GHC’s low-level threading performance.
module Main where import Control.Applicative import Control.Concurrent.MVar import Control.Concurrent import Data.Time import System.Environment main = do mv <- newEmptyMVar start <- getCurrentTime loop mv =<< read . head <$> getArgs end <- getCurrentTime putStrLn $ "creation time: " ++ show (diffUTCTime end start) putMVar mv 0 takeMVar mv fin <- getCurrentTime putStrLn $ "message time: " ++ show (diffUTCTime fin end) loop :: MVar Int -> Int -> IO () loop mv n | n <= 0 = return () | otherwise = do forkIO $ do m <- takeMVar mv putMVar mv $! m+1 loop mv (n-1)
This measures two things: how long it takes to fork a given number of threads, and how long it takes to send a tiny message through this ring of threads.
On my 32-bit desktop, I can fork 175,000 threads in one second, and send a message through the ring in 0.09 seconds. This was with a thread stack size of 172 bytes. GHC’s runtime will grow a thread’s stack dynamically if it overflows, so it’s safe to use such a small stack size.
During these runs, the memory consumed by the process maxed out at a slender 45MB.
I find all of these numbers to be very impressive, but they don’t grow linearly. Memory consumption stays low if I create 1.75 million threads (just 354MB), but the time to fork them leaps up to 95 seconds. Messaging through the ring also takes a big jump, to 5.6 seconds.
There’s an open bug report (by me) about the non-linear scaling of thread operations.
Lennart, I can’t find that ticket. Do you happen to know what its number is?
Great post, thanks for the simple example!
Note: when reading this post over on Planet Haskell, I noticed that the less-than signs are missing. I checked the rss feed directly here, and they appear not to be escaped, nor are they escaped in the HTML for this page (although they display correctly). I recommend updating your code snippit with the “<” mark.
Is there anything in that code which actually ensures that the message will go all the way round? (ie that the measurements will be measuring the correct thing) – ‘takeMVar’ is documented to be FIFO, but isn’t there a danger that the main thread will get its take in before some of the threads?
Nick, thanks for mentioning the formatting problem. I wish WordPress was more predictable.
Ben, it is indeed possible that the main thread might sneak in and get the MVar before any of the other threads starts running, but that would be visible in the measured output.
Lennart’s GHC bug report is here:
http://hackage.haskell.org/trac/ghc/ticket/1589
the non-linear growth of thread creation is due to the garbage collector not having a proper write barrier on threads. Hopefully I’ll get around to fixing it before 6.10.