Summary: IO evaluation caught me off guard when trying to write some benchmarks.
I once needed to know a quick back-of-the-envelope timing of a pure operation, so hacked something up quickly rather than going via criterion. The code I wrote was:
main = do
(t, _) <- duration $ replicateM_ 100 $ action myInput
print $ t / 100
{-# NOINLINE action #-}
action x = do
evaluate $ myOperation x
return ()
Reading from top to bottom, it takes the time of running action 100 times and prints it out. I deliberately engineered the code so that GHC couldn't optimise it so myOperation was run only once. As examples of the defensive steps I took:
- The
actionfunction is markedNOINLINE. Ifactionwas inlined thenmyOperation xcould be floated up and only run once. - The
myInputis given as an argument toaction, ensuring it can't be applied tomyOperationat compile time. - The
actionis inIOso the it has to be rerun each time.
Alas, GHC still had one trick up its sleeve, and it wasn't even an optimisation - merely the definition of evaluation. The replicateM_ function takes action myInput, which is evaluated once to produce a value of type IO (), and then runs that IO () 100 times. Unfortunately, in my benchmark myOperation x is actually evaluated in the process of creating the IO (), not when running the IO (). The fix was simple:
action x = do
_ <- return ()
evaluate $ myOperation x
return ()
Which roughly desugars to to:
return () >>= \_ -> evaluate (myOperation x)
Now the IO produced has a lambda inside it, and my benchmark runs 100 times. However, at -O2 GHC used to manage to break this code once more, by lifting myOperation x out of the lambda, producing:
let y = myOperation x in return () >>= \_ -> evaluate y
Now myOperation runs just once again. I finally managed to defeat GHC by lifting the input into IO, giving:
action x = do
evaluate . myOperation =<< x
return ()
Now the input x is itself in IO, so myOperation can't be hoisted.
I originally wrote this post a very long time ago, and back then GHC did lift myOperation out from below the lambda. But nowadays it doesn't seem to do so (quite possibly because doing so might cause a space leak). However, there's nothing that promises GHC won't learn this trick again in the future.

No comments:
Post a Comment