xnning / EvEff

Efficient Haskell effect handlers based on evidence translation.
BSD 3-Clause "New" or "Revised" License
81 stars 5 forks source link

Local state is not safe #6

Open noughtmare opened 3 years ago

noughtmare commented 3 years ago

E.g. the program:

import Control.Ev.Eff

main :: IO ()
main = print $ runEff $ local 0 $ do
  localModify (+ 1)
  localGet

Prints (when compiled with -O):

0

I think that this is due to common sub-expression elimination. First the code above gets turned into something like:

import Control.Ev.Eff

main :: IO ()
main =
  let !r = unsafePerformIO (newIORef 0)
      !x1 = unsafePerformIO (readIORef r)
      !() = unsafePerformIO (writeIORef r (x1 + 1)
      !x2 = unsafePerformIO (readIORef r)
  in pure x2

And common sub-expression elimination transforms that to:

import Control.Ev.Eff

main :: IO ()
main =
  let !r = unsafePerformIO (newIORef 0)
      !x1andx2 = unsafePerformIO (readIORef r)
      !() = unsafePerformIO (writeIORef r (x1andx2 + 1)
  in pure x1andx2

A simple solution seems to be to mark perform NOINLINE, but that probably affects performance.

noughtmare commented 3 years ago

I have a better solution, we can rewrite Ctl into a monad transformer CtlM which can run IO actions properly. Then you only need one unsafePerformIO at the top level in runCtl.

I have been able to make it work, but I have to clean up my code a bit. Expect a pull request soon.

Update: the change significantly affects some benchmarks, so I am investigating further. But maybe the microbenchmarks are also not representative of real-world scenarios w.r.t. inlining and specialization as Alexis King's talk "Effects for Less" showed.

mmhat commented 3 years ago

@noughtmare I tried to reproduce this with the current HEAD and it works as intended. Can your confirm that?

noughtmare commented 3 years ago

If I compile with optimizations then I can still reproduce it.