[erlang-questions] BEAM micro-optimization: term comparison (> 0) versus pattern-matching

Mikael Pettersson <>
Sun Apr 16 15:41:04 CEST 2017


Mikael Pettersson writes:
 > Suppose a frequently called function returns a positive integer (always a fixnum)
 > and a boolean "success" or "failure" indicator (even in the failure case the return
 > value is significant and will be used).  Which is better from a performance
 > perspective:
 > 
 > 1. Tag the return value, {success, X} or {failure, Y}, and have callers pattern-match
 >    on that
 > 
 > or,
 > 
 > 2. Indicate failure by negating Y, and have callers match on
 > 
 >       X when X > 0 -> success case;
 >       MinusY -> failure case % MinusY =< 0 is implicit
 > 
 > Option 2 avoids the consing overheads of option 1, but I'm worried that the X > 0
 > guard may be less optimized than a plain structural pattern-match.  The last time
 > I checked, term-comparisons would check inline for identity, and otherwise call
 > the general cmp() function which would then do a lot of type-casing.
 > 
 > In my pure Erlang code I'm using option 1, but I'm reimplementing the function
 > as a NIF, and would like to avoid the consing overheads is that's indeed better.

I put together a micro-benchmark module (appended below), and have
the following observations:

- Option 2 above is about 15-25% faster than option 1 above.
- Adding an is_integer/1 guard before the X > 0 in option 2 makes the code slower.
- A major cost in option 1 is having to do case analysis on two atoms (because the
  runtime representation of an atom isn't known until the code is loaded into a
  running VM).  However, replacing the atoms with the fixnums 0 and 1 makes the
  code much slower.
- The best version I've found so far (function 'k' in the bechmark module) eliminates
  the tags, represents one case by wrapping the integer in a cons cell [X | []], and
  the other case as the plain integer.  [X | []] is slightly better than {X} since the
  latter requires an additional load and test in the pattern-matching path.

On my desktop box (OTP-19.3, Ivy Bridge @ 3.5 GHz), typical output from the benchmark is:

> test:all(100000000).
[{f,{2564769,2}},
 {g,{1806006,2}},
 {h,{1999038,2}},
 {i,{3295790,2}},
 {j,{1779385,2}},
 {k,{1504119,2}},
 {l,{1526827,2}}]

f is option 1 above, g is option 2.  h is g with added is_integer/1 check.
i is f but with integer tags not atoms.  j wraps the two cases as [X|[]] and {Y}.
k is j but with Y left unwrapped.  l is j but swaps the order and checks with
is_integer/1 before matching on [Y|_].

/Mikael

==snip==
-module(test).
-export([all/1, f/1, g/1, h/1, i/1, j/1, k/1, l/1]).

all(N) ->
  L = [ {f, fun f/1}
      , {g, fun g/1}
      , {h, fun h/1}
      , {i, fun i/1}
      , {j, fun j/1}
      , {k, fun k/1}
      , {l, fun l/1}
      ],
  [{A, F(N)} || {A, F} <- L].

f(N) -> timer:tc(fun() -> f2(N, [], []) end).
g(N) -> timer:tc(fun() -> g2(N, [], []) end).
h(N) -> timer:tc(fun() -> h2(N, [], []) end).
i(N) -> timer:tc(fun() -> i2(N, [], []) end).
j(N) -> timer:tc(fun() -> j2(N, [], []) end).
k(N) -> timer:tc(fun() -> k2(N, [], []) end).
l(N) -> timer:tc(fun() -> l2(N, [], []) end).

%% f: tag with {success, X} or {failure, Y}

f2(0, X, _Y) -> X;
f2(N, X, Y) ->
  case f3(N) of
    {success, A} -> f2(N - 1, A, Y);
    {failure, B} -> f2(N - 1, X, B)
  end.

f3(N) ->
  case N band 1 of
    0 -> {success, N};
    _ -> {failure, N}
  end.

%% g: indicate success or failure via the sign

g2(0, X, _Y) -> X;
g2(N, X, Y) ->
  case g3(N) of
    A when A > 0 -> g2(N - 1, A, Y);
    MinusB -> g2(N - 1, X, -MinusB)
  end.

g3(N) ->
  case N band 1 of
    0 -> N;
    _ -> -N
  end.

%% h: like g, but check is_integer before checking > 0

h2(0, X, _Y) -> X;
h2(N, X, Y) ->
  case g3(N) of
    A when is_integer(A), A > 0 -> h2(N - 1, A, Y);
    MinusB -> h2(N - 1, X, -MinusB)
  end.

%% i: like f, but tag with integers 0 and 1 instead

i2(0, X, _Y) -> X;
i2(N, X, Y) ->
  case i3(N) of
    {0, A} -> i2(N - 1, A, Y);
    {1, B} -> i2(N - 1, X, B)
  end.

i3(N) ->
  case N band 1 of
    0 -> {0, N};
    _ -> {1, N}
  end.

%% j: like f but drop the tags, instead tag as [X|[]] for success or {Y} for failure

j2(0, X, _Y) -> X;
j2(N, X, Y) ->
  case j3(N) of
    [A | _] -> j2(N - 1, A, Y);
    {B} -> j2(N - 1, X, B)
  end.

j3(N) ->
  case N band 1 of
    0 -> [N | []];
    _ -> {N}
  end.

%% k: like j, but only tag the success case not the failure case

k2(0, X, _Y) -> X;
k2(N, X, Y) ->
  case k3(N) of
    [A | _] -> k2(N - 1, A, Y);
    B -> k2(N - 1, X, B)
  end.

k3(N) ->
  case N band 1 of
    0 -> [N | []];
    _ -> N
  end.

%% l: like k but the other way around: tag the failure case [Y|[]],
%% check for success with is_integer/1

l2(0, X, _Y) -> X;
l2(N, X, Y) ->
  case l3(N) of
    A when is_integer(A) -> l2(N - 1, A, Y);
    [B | _] -> l2(N - 1, X, B)
  end.

l3(N) ->
  case N band 1 of
    0 -> N;
    _ -> [N | []]
  end.
==snip==


More information about the erlang-questions mailing list