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.