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.