[erlang-questions] Heads-up: The cost of get_stacktrace()
Richard Carlsson
carlsson.richard@REDACTED
Tue Nov 5 21:36:15 CET 2013
(Executive summary: exceptions cheap, but erlang:get_stacktrace() kind
of expensive; also, avoid 'catch Expr'.)
We have wrestled for some time with some very strange unresponsiveness
and high amounts of garbage collection, and finally managed to track
down the problem. It was in a piece of code that matches some input data
against a number of different "patterns", trying one possibility at a
time in a failure-driven loop. The actual problem was a wrapper that
executed each call in a try/catch, with a default catch clause looking
something like this:
try match(Pattern, Data)
catch
...
Class:Term ->
{foo_error, {Class, Term, erlang:get_stacktrace()}}
end
The stacktrace was usually discarded again further up and the whole
thing was retried with another "pattern". This code got executed tens of
thousands of times per second. When we removed the call to
get_stacktrace(), the system instantly started to behave much better.
The purpose of this mail, then, is both to warn about sloppy use of
get_stacktrace() and to clarify how stack traces are handled and wherein
the costs lie.
First of all, triggering an exception is quite cheap. The necessary
stack trace information (by default, 8 pointers) is quickly saved in an
opaque blob, and control gets passed up to the nearest catch handler (if
there is one). If there is a handler, normal Erlang execution will
resume, trying to match the catch-clauses. If no catch clause matches,
the exception state is unchanged and we look for the next catch handler,
until either some catch clause matches or the top of the call stack is
reached (which will terminate the process).
If a catch clause matches, execution just continues and no extra cost is
incurred as long as you don't try to inspect the stack trace. If none of
the clauses match, the only cost was that of trying the clause patterns
and guards. For example, terminating a process by "exit(normal)" has
very little overhead even if it passes through a number of catch
handlers that just pass it on upwards, because even the process exit
signal will not contain the stack trace. And using throw/catch for
nonlocal return out of a deep recursion is very cheap.
*But* if someone wants to actually look at the stack trace of the
exception, the "opaque blob" mentioned above must be reified as an
Erlang term, by calling erlang:get_stacktrace(). This amounts to looking
up the module and function name and arity corresponding to each of the
saved code pointers, and creating a corresponding list of MFA tuples on
the heap. (This also happens if the process terminates due to an
exception of type 'error' or 'throw', to include the stack trace in the
exit signal.)
In addition, as of Erlang/OTP R15 this operation is 4-5 times(!) more
expensive than it used to be pre-R15, because now the stack trace also
includes file names and line numbers. That's more data to be allocated
on the heap, but most of the cost is probably in traversing the tables
that map bytecode regions to corresponding source file regions (these
tables are created by the compiler and are included in the .beam files).
For us, this difference meant that we went from "mysteriously high
activity, but not critical" under R14 to "random bursts of
unresponsiveness" under R15, and it took us a lot of effort to figure
out what was going on.
So the general advice is: Don't call erlang:get_stacktrace() just
because you can. If you don't have a real reason for catching every
possible exception, just let the uninteresting ones fall through. Avoid
the temptation to have a catch-all clause like in the example above,
that re-packages the exception wrapped in a tuple with some tag that you
happen to like. In particular if there's a chance that the code will be
re-tried over and over again. If you don't intend to handle the
exception, then let it remain an exception for as long as possible and
don't turn the stack trace into a term, because that's when you pay.
It's of course still valid to call get_stacktrace() in many situations,
e.g. when the process is on its way to give up, or to write the crash
information to a log, or for something that only happens rarely and the
stack trace information is useful - but never in a library function that
might be used heavily in a loop.
Finally, this is also another reason to rewrite old occurrences of
'catch Expr' into 'try Expr catch ... end', because it basically works
like this:
try Expr
catch
throw:Term -> Term;
exit:Term -> {'EXIT', Term};
error:Term -> {'EXIT', {Term, erlang:get_stacktrace()}}
end
so what happens if you use one of the following old idioms?:
...
catch foo(...), % for side effect, ignore the result
...
or
case catch foo(...) of
{'EXIT', Reason} -> ...;
Result -> ...
end
Well, when the exception type is 'error', the catch will build a result
containing the symbolic stack trace, and this will then in the first
case be immediately discarded, or in the second case matched on and then
possibly discarded later. Whereas if you use try/catch, you can ensure
that no stack trace is constructed at all to begin with.
Sorry that this got a bit long, but I think that was all.
/Richard
More information about the erlang-questions
mailing list