Timing expressions in Elixir

Have you ever added or updated a piece of code and wondered, how long does this take to run? Does it take a millisecond? An hour? How does the time change if parameters are nudged one way or another?

These are common benchmarking questions. For all serious benchmarking needs, Benchee is the go-to tool. But what if you want something less serious? Something simpler? In that case, Erlang has us covered with the function :timer.tc. Wrap the expression you want to time in a zero-arity anonymous function (a thunk) and pass it to :timer.tc:

my_fun = fn i ->
  1..i
  |> Enum.map(&(&1 * 2 / ((1 + i) * i)))
  |> Enum.sum()
end

{time_in_microsec, result} = 
  :timer.tc(fn -> my_fun.(100_000) end)

:timer.tc returns a 2-tuple containing the execution time in microseconds and the result of the function.

This is great! I've used this Erlang nugget many times. But I think the ergonomics could be improved. Having to wrap the expression in a thunk is a bit tedious. And if we want some timing statistics after a few runs, we'll have to include those extras ourselves. Finally, wouldn't it be great to have the simplicity of IO.inspect, where the expression is just piped into a timer?

I wrote Tim (the tiny timer) to satisfy my ergonomic desires for a timer. Tim is extremely simple. It has one function, time, that takes any Elixir expression and an optional number of runs, and returns a map that holds some statistics for the execution time, the execution result, and the expression as a string.

Pipe an expression into Tim.time as follows:

require Tim

my_fun.(100_000)
|> Tim.time()


# example return

%{
  expr: "my_fun.(100_000)",
  max: 59226, mean: 59226.0, 
  median: 59226, 
  min: 59226, 
  n: 1, 
  result: 1.0
 }

Just like :timer.tc, Tim returns times in microseconds. Also, notice that require Tim is needed before Tim can be used. This is because Tim.time is a macro.

Want to try different inputs with a few timing runs for each choice? Try this:

for i <- [1000, 100_000] do
  my_fun.(i)
  |> Tim.time(3)
end


# example return

[
  %{
    expr: "my_fun.(i)",
    max: 2031,
    mean: 1763.3333333333333,
    median: 1647,
    min: 1612,
    n: 3,
    result: 1.0
  },
  %{
    expr: "my_fun.(i)",
    max: 113456,
    mean: 91578.33333333333,
    median: 82055,
    min: 79224,
    n: 3,
    result: 1.0
  }
]

As already noted, Tim.time is a macro. This macro takes in the expression being timed, wraps it in a thunk at compile time, and passes the thunk to :timer.tc. Pretty simple.

I like small utilities like this. Sometimes they're quality-of-life enhancements, like Tim. But other times, their value is more pedagogical. What better way to really understand how something works than to build a small version of it yourself? I think I'll build more tiny tools.