
Size cost of C++ exception handling on embedded platforms - Tatyanazaxarova
https://andriidevel.blogspot.com/2016/05/size-cost-of-c-exception-handling-on.html
======
willvarfar
In a compiler's intermediate representation, exceptions are typically modeled
as multiple returns. E.g. in LLVM the `invoke` op specifies a label to go to
if an exception is returned [http://llvm.org/docs/LangRef.html#invoke-
instruction](http://llvm.org/docs/LangRef.html#invoke-instruction)

By the time this reaches the backend, the exception handling is usually
converted into "zero cost" exceptions where raising an exception calls a
handler which uses a lookup table to work out how to unwind the stack and what
destructors to run etc. Here's a really good explanation I found
[https://mortoray.com/2013/09/12/the-true-cost-of-zero-
cost-e...](https://mortoray.com/2013/09/12/the-true-cost-of-zero-cost-
exceptions/)

This "zero cost" exception handling has _no performance penalty_ on normal
CPUs in normal execution as exceptions are exceptional so the conditional call
to the handler easy on the branch predicter and the clutter of unwinding
doesn't fill the caches.

(Zero cost execption handling appeared first iirc in Metrowerks compilers, but
quickly became the standard way on most modern platforms and is now in the
ABIs.)

The multiple-return approach may well be much better for total program size.

Whether you can use the multiple-return approach on bare ARM - when the ARM
ABI specifies zero cost exception handling - is nothing I know about.

~~~
brandmeyer
GCC does employ DWARF exception handling (the zero-cost EH model) on ARM7TDMI.
While the cost is zero in time, it is not zero in space. The EH tables,
unwinder code, and support code for them have a non-zero space cost. While the
space cost is normally considered negligible in a workstation or server
environment, in a microcontroller environment it most certainly is not.

~~~
choosername
well, if ypu want exceptions that practically don't exist neither in time nor
space, use something else :)

------
Rexxar
This article is very interesting but it address only the constant overhead. It
would be interesting to know is there is also a factor on the size of the
program (Sexp = k*Snoexp + C). I'm not sure if this factor (here 'k') will be
greater or smaller than 1 (without exception, you have to generate code for
every if/then needed for managing the error code).

If k<1, the choice become a trade-of : Small programs are smaller without
exception and big programs are smaller with exception.

Has anyone try to measure this factor ?

------
dkopi
Exceptions are a really valuable programming tool , but you can live without
them. The cost of not having exceptions is usually giving every function in
your code the option to return an error value.

It takes some time getting used to, but it also creates the habbit of checking
error codes on every function you call. It also makes handling errors
something you do as soon as possible (and in the most recoverable way).

~~~
curryhoward
True, and a good type system will enforce that you check for errors. However,
depending on how often errors occur in your program, this can actually make
your code slower. Good exception handling implementations are "zero-cost",
meaning that there is no performance impact in the happy path (when no
exceptions are raised). C-style error handling (an if statement) is actually
slower, at least in non-embedded environments.

~~~
haldean
In C, there are ways you can get the optimizer to turn if-based error handling
into something that only spends one or two instructions; using
__builtin_expect[0] can decrease the cost of a condition by letting the branch
predictor pick the "right" thing to expect. That way you end up only paying
the price of the branch (and maybe the jump) instructions without any pipeline
flushes or stalls.

[0]: [http://blog.man7.org/2012/10/how-much-do-builtinexpect-
likel...](http://blog.man7.org/2012/10/how-much-do-builtinexpect-likely-
and.html)

------
userbinator
I think this shows clearly that compilers are still quite bad at "optimising
in the small", since they tend to all be based on the "link in everything and
let later passes strip out some of it" principle, whereas what's really needed
is for them to link in code only if it can be proved it will be needed/can't
be proved to be unneeded. And even after his efforts at removing unused code,
there's still plenty remaining (more than order of magnitude difference.)
Ideally the compiler should be able to optimise the exception code to become
identical to the no-exceptions code, something that a human could easily do in
this case but the compiler couldn't.

~~~
TickleSteve
Link Time Optimisation is what you're referring to (-flto) and that wont solve
the issue. exception handling is inherently unsuited for embedded systems.

The size & performance overhead is one thing, but the model of attempting to
"handle" errors is incorrect. In most cases, a PANIC situation should lead to
a watchdog reset.

Exceptions should be for _exceptional_ cases, not the usual program logic. In
embedded systems, _exceptional_ cases == reset.

~~~
tremon
_Exceptions should be for exceptional cases_

Why? Simply because the words look alike?

I think you're painting a false dichotomy here. There's a whole world of
program state between "usual program logic" and "exceptional cases".

~~~
TickleSteve
defined recovery cases are not exceptions.

exceptions are breaks from what you had assumed to be constant.

There is a reason program logic is not placed into exception handlers... the
readability would be completely destroyed.

~~~
barrkel
You're thinking of asserts, not exceptions.

Exception usage can usually be split into three buckets: (1) programming
errors where abort() or equivalent isn't suitable (e.g. libraries); (2)
semantic errors at the application level where language provided unwinding is
used as a convenience to jump back to the core event / request loop and
provide an error to the user or client; and (3) to provide out-of-band error
information for failures when interacting with non-deterministic systems (e.g.
failure to open a file or communicate with a device, where the natural
function to write returns a value, rather than a success or error code).

There are alternative solutions to all three, and all three may not apply to
every environment. For embedded systems, case (1) may indeed not apply. But
cases (2) and (3) may be useful as a programming convenience to automate the
idiom of checking error conditions and aborting the current operation. If
checking error conditions and aborting is fully automated (like monadic Result
handlers in e.g. Rust) then you start approaching an isomorphic semantics to
exceptions, with no necessary difference in implementation details.

~~~
TickleSteve
nope, definitely not thinking of asserts, they are nothing to do with this
situation.

program logic encoded in an exception handler is undeniably less easy to read
than explicitly coded error cases.

Also, the non-locality of the decision making means the further away from the
error-site you are, the more context you have to keep in your head.

This similar to what deep inheritance hierarchies suffer from, non-local
logic. you end up jumping all around your source tree trying to figure out the
full context that an error has occurred in.

~~~
kbenson
That's a very strong statement. You really believe there can't possibly exist
_any_ instance where program logic in an exception handler ends up being
easier to read? That assumes you know enough about every possible permutation
of logic flow to know that exceptions could not benefit it.

~~~
TickleSteve
Of course not, I'm talking about a general rule.

No rules are universal.

~~~
kbenson
Sure. The real point I'm getting at is that overly strong statements lead to
arguments, where people take your statement at face value. Hyperbole is rarely
useful in a serious discussion. It just means people have to work to determine
your real stance because it may not match exactly what you said.

You could have said "I've never seen a case where exception handling resulted
in more clear and easier to read code, and I doubt I'll ever encounter such a
situation" and I think that would convey your opinion clearer (assuming I
understand it correctly).

> No rules are universal.

We're talking about CS and programming here, where there are plenty of cases
where things have been formally proven. Some rules _are_ universal. No reason
to use absolutist statements where they don't apple.

------
bhouston
How much is this space increase because of dependences upon the STL library
that is included to do the exception handling versus the actual exception
hanlding itself.

I would like to see a comparison that doesn't use STL for the exceptions in
C++ and then measure that difference.

I find that the use of STL increases the size of the resulting binary
significantly by itself.

(Now maybe since STL is the official C++ library maybe this is still a fair
comparison, but STL is known for bloating the size of binaries.)

~~~
tacos
I realize we're talking embedded here, but what is "bloat" in the context of
2016? I have a suite of 35 command line apps with header-only dependencies on
Boost and the whole shebang builds in under ten seconds on 2012 MacBook. Many
of the binaries (64-bit) are under 200k.

EDIT: I've hand-coded 68xx, 56xxx and 8051 when 4K RAM was a luxury. But this
"C++ is bloat" and "exceptions are universally wrong" discussion feels a
little 1995 to me. It's 2016: if you have software it's not that hard to find
a $5 system to run it on. And people who actually write embedded code know how
to pick their tools. And at least one of them isn't necessarily afraid of STL
or exceptions... me.

EDIT 2: jotux comment below nails it.

~~~
Paul_S
I think we have different ideas about what an embedded environment is if a
macbook is your reference platform.

~~~
tacos
"64-bit" and "using Boost" are the clues that I was making a contrasting, not
supporting, argument.

------
drivebyops
Welp time to use Option types for error handling ;)

~~~
nwmcsween
Rust still requires stack unwinding info (dwarf CFI) so it's still going to be
bloated on anything non-trivial.

~~~
steveklabnik
You can turn off unwinding as of a few days ago in nightly. We'll see how long
it takes to hit stable.

~~~
nwmcsween
This is one of my issues with Rust, there are a a couple dozen knobs that need
to be turned and may or may not break things to get somewhat sensible behavior
(imo). And creating a shared library with Rust with it's default behavior of
crashing on OOM (Linux can be configured to not do overcommit), etc is just
plain bad.. oh yes there is a knob for that too!

------
golergka
But still, it's a x49 increase between code with exceptions and without. It
would be logical to assume that additional exception handling code is the same
regardless of the original code size — but I'm not sure about how correct such
an assumption is.

~~~
scatters
The exception support code is a constant overhead. The remaining difference in
size will come from the unwind tables - but it's difficult to compare apples
with apples there, because a program written to not use exceptions will need
additional error handling logic, which has its own significant size overhead.

------
TickleSteve
Exceptions are the wrong model for embedded systems, generally if something
_exceptional_ happens, you need a reset. Attempting to "handle" exceptional
cases is incorrect, a reset is the correct option.

Exceptions != normal program flow.

~~~
tremon
You can divide embedded systems between industrial control systems and
headless consumer devices, like set-top boxes or smart TVs. For the latter,
you really don't want to invoke a device reset every time the input stream is
malformed.

~~~
TickleSteve
true, but I would not consider a set-top tv a true embedded system, rather a
specialised user-facing system. So yes, you wouldn't reset a user facing
system.

I would characterise embedded systems as those having to maintain
functionality without user intervention.

(also, comms issues such as input stream errors are the normal program logic,
i.e. defined error cases).

------
hellofunk
I'd also be curious about the run-time costs (performance) of an exception-
ready system that doesn't actually ever handle exceptions vs one that doesn't
consider exceptions a possibility.

------
jokoon
I'm a beginner, but why use exceptions instead of returning an error code and
displaying a message?

~~~
exception_e
Here's a great StackOverflow question/answer on this:
[http://stackoverflow.com/questions/4670987/why-is-it-
better-...](http://stackoverflow.com/questions/4670987/why-is-it-better-to-
throw-an-exception-rather-than-return-an-error-code)

I program in Java and I like that methods have __* to declare what they
exceptions they throw. This makes it very obvious from a consumer standpoint.

 __* except for runtime exceptions

~~~
cletus
Checked exceptions were/are one of Java's biggest mis-features (IMHO). You
can't force a client to deal with exceptions. Checked exceptions ends up
encouraging empty catch blocks, which is usually worse than propagating an
exception.

~~~
exception_e
If the method you're designing is going to throw an exception that wouldn't
make sense to handle from a consumer standpoint, creating an exception that
extends `RuntimeException` would be my advice. I agree that empty catch blocks
are not a good idea.

~~~
daemin
It would b a good idea to catch it. Except Java has a FileNotFound exception
when trying to open something, which to me should just be part of the normal
control flow and is not something exceptional.

That and you tend to get code that just "throws Exception" propagated all the
way down to main. I know I've done it for little projects at Uni (when I last
programmed in Java, 3.0 I think?)

------
cokernel_hacker
I recently reimplemented machinery in our compiler which implements C++
exceptions. I figure it might be useful to share a few interesting issues.

The way C++ exceptions are used creates very interesting interactions in the
design of their implementation.

There a hidden cost many forget: call frame information restricts what the
compiler can do. Why is this the case? Even if exceptions are super rare, the
compiler still needs to make sure the program will obey the language rules if
an exception is somehow thrown at an appropriate program point.

The compiler must emit instructions which can be represented by the call frame
information in order for the unwind to be able to reason about the stack.

This is usually not a problem on Linux because the DWARF CFI is very
expressive. That expressiveness comes at a cost: it is quite inefficient when
it comes to size.

Other platforms, Windows NT (ARM, x64 and IPF) and iOS, recognized that this
increase in size is a bad state of affairs and thus aimed to make the CFI more
compact. By doing this, they greatly reduced the size of CFI but unfortunately
created restrictions on what a compiler can do.

As for trickiness inherent in C++ exceptions, C++ supports a fairly esoteric
feature: exceptions may be rethrown without being in a try or catch:

    
    
      void rethrow() {
        throw;
      }
    

An easy way to make this sort of thing work would be to thread a secret in/out
parameter which represents the exception state.

But how is this typically implemented?

Well, remember, the ethos of exceptions in C++ is that they are rare. Rare
enough that implementors are discouraged from optimizing the speed of C++
exceptions.

Instead, thread local storage is typically used to go from any particular
thread back to it's context.

Things get pretty darn complicated pretty quickly with features like dynamic
exception specifications:

    
    
      void callee() throw(double) {
        throw 0;
      }
      void caller() {
        try {
          callee();
        } catch (...) {
          puts("got here!");
        }
      }
    

On first examination, "got here!" should be unreachable because the call to
"callee" results in a violation of the exception specification.

However, this is not necessarily the case! What _actually_ happens is that
some code runs between the throw and the catch: std::unexpected is called.

Now, std::unexpected might throw an exception of it's own! If this new
exception matches the exception specification, the exception would pass into
the catch block in "caller". If it doesn't, the exception thrown within
std::unexpected might result in another violation!

Wow, this is get complicated... OK, so what happens if it results in another
violation? Well, the exception gets cleaned up and replaced with, you guessed
it, another exception! We'd be left with an exception of type
std::bad_exception leaving std::unexpected and thus "callee". Because the
catch clause in "caller" is compatible with std::bad_exception, control is
transferred to the catch block.

This is the tip of the iceberg. A huge amount of machinery is sitting around,
waiting to engage to make exceptions work.

------
MichaelMoser123
C++ exceptions have a runtime overhead, because they have to set up a handler
(insert a record into a linked list) upon entering the try block and remove it
when leaving the try block. Now of course checking return values introduces a
lot of branches, so go figure out which one of them is worse...

still it is very advisable to have likely pragmas around if conditions that
check return values.

The big problem with exceptions is that it introduces a lot of implicit code
paths, and lots of possibilities for resource leaks (if raw pointers are
involved).

~~~
mzs
That's one way they can be implemented, but now they tend to be zero-cost
implementation where only when an exception occurs does it figure-out where it
was and what to do.

~~~
MichaelMoser123
the article speaks about 'embedded' systems - these are not x84-64 systems
(intel cpus need too much power) so they don't have a 'zero cost' exception
table (that is also not exactly zero cost)

Can you show me an embedded system with zero cost exceptions?

~~~
mzs

      $ CXX=g++ppc CXXFLAGS=-g make -f /dev/null foo.o
      g++ppc -g   -c -o foo.o foo.cc
      $ objdumpppc -drS foo.o                         
      
      foo.o:     file format elf32-powerpc-vxworks
      
      
      Disassembly of section .text:
      
      00000000 <_Z3fooi>:
      int
      foo(int i)
         0:   94 21 ff d0     stwu    r1,-48(r1)
         4:   7c 08 02 a6     mflr    r0
         8:   90 01 00 34     stw     r0,52(r1)
         c:   93 e1 00 2c     stw     r31,44(r1)
        10:   7c 3f 0b 78     mr      r31,r1
        14:   90 7f 00 18     stw     r3,24(r31)
      {
              try { if (i < 0) throw (i); }
        18:   80 1f 00 18     lwz     r0,24(r31)
        1c:   2f 80 00 00     cmpwi   cr7,r0,0
        20:   40 9c 00 74     bge-    cr7,94 <_Z3fooi+0x94>
        24:   38 60 00 04     li      r3,4
        28:   48 00 00 01     bl      28 <_Z3fooi+0x28>
                              28: R_PPC_REL24 __cxa_allocate_exception
        2c:   7c 60 1b 78     mr      r0,r3
        30:   7c 0b 03 78     mr      r11,r0
        34:   7d 69 5b 78     mr      r9,r11
        38:   80 1f 00 18     lwz     r0,24(r31)
        3c:   90 09 00 00     stw     r0,0(r9)
        40:   7d 63 5b 78     mr      r3,r11
        44:   3d 20 00 00     lis     r9,0
                              46: R_PPC_ADDR16_HA     _ZTIi
        48:   38 89 00 00     addi    r4,r9,0
                              4a: R_PPC_ADDR16_LO     _ZTIi
        4c:   38 a0 00 00     li      r5,0
        50:   48 00 00 01     bl      50 <_Z3fooi+0x50>
                              50: R_PPC_REL24 __cxa_throw
        54:   90 7f 00 1c     stw     r3,28(r31)
        58:   7c 80 23 78     mr      r0,r4
        5c:   2f 80 00 01     cmpwi   cr7,r0,1
        60:   41 9e 00 0c     beq-    cr7,6c <_Z3fooi+0x6c>
        64:   80 7f 00 1c     lwz     r3,28(r31)
        68:   48 00 00 01     bl      68 <_Z3fooi+0x68>
                              68: R_PPC_REL24 _Unwind_Resume
      
              catch (int e) { i = -e; }
        6c:   80 7f 00 1c     lwz     r3,28(r31)
        70:   48 00 00 01     bl      70 <_Z3fooi+0x70>
                              70: R_PPC_REL24 __cxa_begin_catch
        74:   7c 60 1b 78     mr      r0,r3
        78:   7c 09 03 78     mr      r9,r0
        7c:   80 09 00 00     lwz     r0,0(r9)
        80:   90 1f 00 08     stw     r0,8(r31)
        84:   80 1f 00 08     lwz     r0,8(r31)
        88:   7c 00 00 d0     neg     r0,r0
        8c:   90 1f 00 18     stw     r0,24(r31)
        90:   48 00 00 01     bl      90 <_Z3fooi+0x90>
                              90: R_PPC_REL24 __cxa_end_catch
      
              return (i);
        94:   80 1f 00 18     lwz     r0,24(r31)
      }
        98:   7c 03 03 78     mr      r3,r0
        9c:   81 61 00 00     lwz     r11,0(r1)
        a0:   80 0b 00 04     lwz     r0,4(r11)
        a4:   7c 08 03 a6     mtlr    r0
        a8:   83 eb ff fc     lwz     r31,-4(r11)
        ac:   7d 61 5b 78     mr      r1,r11
        b0:   4e 80 00 20     blr
      $ g++ppc -v 2>&1 | tail -1                      
      gcc version 4.3.3 (Wind River VxWorks G++ 4.3-315) 
      $

