Sometimes I’m writing an IO loop in Haskell, and I need some sort of a counter or accumulator. The two main options are to use a mutable reference (IORef) or to put a StateT transformer on top the IO monad.
I was curious, though, if there was a difference in efficiency between these two approaches. Intuitively, IORefs are dedicated heap objects, while a StateT transformer’s state becomes “just” a local variable, so StateT might optimize better. But how much of a difference does it make?
So I benchmarked the four functions, all of which calculate the sum of numbers between 1 and
n = 1000.
base_sum simply calls
sum from the base package;
stateT_sum maintain the accumulator using the
State Int and
StateT Int IO monads, respectively, and
ioref_sum uses an
IORef within the
IO monad. And here are the results, as reported by criterion.
I’m not sure how
stateT_sum manages to be faster than
base_sum (this doesn’t appear to be a statistical fluke), but what’s clear is that
ioref_sum is significantly slower of them all.
So if 3ns per state access matter to you, go for
StateT even when you are in
(Update: also check out the comments on reddit, especially the ones by u/VincentPepper.)
Here’s the full benchmark code. It was compiled with
-O2 by GHC 8.8.4 and run on AMD Ryzen 7 3700X.
import Criterion import Criterion.Main import Control.Monad.State import Data.IORef base_sum :: Int -> Int = sum [1 .. n] base_sum n state_sum :: Int -> Int = flip execState 0 $ state_sum n 1..n] $ \i -> forM_ [+i) modify' ( stateT_sum :: Int -> IO Int = flip execStateT 0 $ stateT_sum n 1..n] $ \i -> forM_ [+i) modify' ( ioref_sum :: Int -> IO Int = do ioref_sum n <- newIORef 0 ref 1..n] $ \i -> forM_ [+i) modifyIORef' ref ( readIORef ref = do main let n = 1000 defaultMain"base_sum" $ whnf base_sum n [ bench "state_sum" $ whnf state_sum n , bench "stateT_sum" $ whnfAppIO stateT_sum n , bench "ioref_sum" $ whnfAppIO ioref_sum n , bench ]