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.