Side effects make profiling simple: at any time, you can just ask for the time now, ask for the time later, and compute the difference. For pure code, this option isn’t readily available. Fortunately, there are other methods for profiling. Performing periodic stack traces can offer a rough but representative view of where a program spends its time. However, I’m not aware of any profiling method that beats the ad-hoc flexibility of stopwatch style profiling with side effects.
Today I had an interesting, simple idea to support stopwatch style timers fully within pure code. The idea relies on opaque ‘timer’ values. Like a stopwatch, our timer can be active or paused. In either case, I can use a simple integer to represent the time values, perhaps with microsecond precision. Here’s a rough sketch of the code (in pseudo Haskell):
newtype ActiveTimer = Active !Int64 -- microseconds since epoch newtype PausedTimer = Paused !Int64 -- active microseconds elapsed newTimer :: PausedTimer newTimer = Paused 0 startTimer :: PausedTimer → ActiveTimer startTimer = unsafePerformIO . startTimerIO stopTimer :: ActiveTimer → PausedTimer stopTimer = unsafePerformIO . stopTimerIO startTimerIO :: PausedTimer → IO ActiveTimer startTimerIO (Paused n) = Active . (- n) <$> getTime stopTimerIO :: ActiveTimer → IO PausedTimer stopTimerIO (Active n) = Paused . (- n) <$> getTime unsafeReadTimerIO :: PausedTimer → IO Int64 unsafeReadTimerIO (Paused n) = pure n getTime :: IO Int64 -- microseconds since epoch
The timers are first-class plain old immutable values. But we’re restricted on how we may safely observe them. The main thing we can safely do with timers is print them as part of a Debug `trace` message.
The purely functional language I’m developing does not have unsafePerformIO. But I do have annotations to address debugging and performance issues, and support for explicit stopwatch style profiling seems a reasonable application. It is trivial for a runtime to provide a local equivalent to newTimer, startTimer, and stopTimer. Rendering resulting timers would require routing through an IO model. Or printing a debug ‘trace’ message of some sort.
Addendum: I am concerned that these timers may hinder caching and structure sharing. I could potentially handle this by simply forbidding timers from contexts where they cause trouble. But I’m reluctant to do so. I might end up favoring second-class named timers, manipulated by annotation.