Size cost of C++ exception handling on embedded platforms
andriidevel.blogspot.com
andriidevel.blogspot.com
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...
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.
llvm.invoke function-to-call, block-if-success, block-if-failure
The backend translates this into: function-to-call
block-if-success:
....
Notice that there is no conditional. However, if in function-to-call, you have: throw exception()
This will be translated into a table lookup that checks which exception handler is valid for the current block. This is the slow part and is generally no worse (theoretically) than frame-based exception handling.So basically, assume it is fast as hell (zero-cost) until you hit an exceptional case (so don't use exceptions for flow control.) In fact, I no longer use exceptions at all. I just "make a note and move on". Having objects that don't cause catastrophic failures when they are "null" is a good first step.
Just a data-point and a good case of profiling beating intuition :)
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 ?
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).
[0]: http://blog.man7.org/2012/10/how-much-do-builtinexpect-likel...
Option/Result are really well designed and let you express all sorts of wonderful flow. Being able to .and_then(), .or_else() or the combination of them and many others is just sublime. The doc page goes into quite a bit of detail: https://doc.rust-lang.org/book/error-handling.html
To be fair, there are compiler flags to check this. And if not, there are static analysis tools that do the same.
There's also the [[nodiscard]] attribute of c++17.
My experience has been that error paths have a way of getting things into inconsistent states, and then you're in trouble regardless.
Checking return codes is annoying but if you're wanting to write robust software, your code needs to be in some way aware of every possible error path.
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.
But in the RPC thread is a lot more like non-embedded code: if I detect an error deep in the stack, I can just throw an exception and catch at the top level. The PC gets an error message and the embedded system treats it as a no-op.
Placing program logic into exception handlers is obfuscation.
As best I understand your argument, it is circular. You assert that if it is an exception, then the only thing to do is reset. When I give a counter-example, you define it as "not an exception" -- presumably because a reset is not desirable.
I agree that defined recovery cases are part of the program logic. But what reason -- beyond a slogan, do you have for reject exceptions as a tool for implmenting that part of the logic? How is it "obfuscation" to do the catch-and-report in the RPC front-end?
The danger I know of with exceptions is that throw sites are invisible, which makes it easy to neglect clean-up actions on error paths. Is that what you meant?
If you don't have a defined recovery process, that technically would be an exceptional case... but because of the nature of embedded systems, a reset is the only reasonable recovery mechanism (a catch all).
Therefore, there is no place for exception handling in embedded systems (as a general rule. Of course there are exceptions... ;o)
The dislike of exception handlers is also that they harm readability, The further you get away from a call-site the more context you have to hold in your head making it more complex.
Keeping error recovery local to the logic improves everything, from readability to code size to complexity and performance.
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".
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.
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.
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.
For which category of exceptions? For interaction with non-deterministic systems where you can make a localized decision - and this is fairly rare - I'd agree with you. For all the other categories, I think you're wrong. If you never used exceptions in the other ways, this will of course colour your thinking.
No rules are universal.
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.
protocol stack logic is just that... logic, no need to short circuit returns with the use of exceptions.
Exceptional case are things that you have a-priori determined to be constant, i.e. the existence of a FLASH device, an RTC working correctly for example. If any of those devices fail, it could be considered an exceptional case, therefore the only action left to you is to reset and hope the condition clears. boot-loops obviously have to be handled also.
There is a world of difference between defined recovery cases and just throwing an exception because you dont know how to "handle" it.
To run with your example, I could imagine handling a missing storage device in lots of different ways, including a) Retry the operation b) Alert the user and block until a device (re)appears c) Switch to an alternative storage location d) Replace the to-be-read-in values with some sensible defaults.
It seems to me that it would be easier to write this logic once and place it in an exception handler than it would be to wrap every I/O operation in a thicket of if/else clauses.
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.)
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.
I've recently (last 3 years) moved my bare-metal embedded software development from C to C++ so I've actually run into issues of STL adding too much to the binary size.
To give a real-world example, I'm working on a custom device that is a type of data cartridge. I have a "big" ARM-A5(Atmel SAMA5D36) processor running embedded linux that does all the heavy lifting but goes down when main power is removed. I have a small ARM-Cortex-M0(LPC824) that is always running on a battery and manages power, watches for button-presses, and a few other janitorial items on the device. The small processor has 32kB of program space and 8kB of RAM, and my project is mostly C++. Here are some specific examples of how using any STL will bloat program size:
Using std::string, and touching any of the STL string handling immediately adds 20kB to my binary. Using stl::list is 6-10kB of program space (actually not that bad and std::vector is pretty efficient). Adding exception handling adds 5kB of code and a single exception adds 6kB of code (not stl but just an example).
I've worked on a lot of embedded projects and my general rule of thumb for, "How big of a part do I need to use fully-featured C++?" is 256kB of program space -- something most bare-metal embedded software engineers would call a large amount of program memory. On projects where I have less than that I don't use STL and basically use C++ as C with classes, function/operator overloading and templates.
Even better, how about getting info directly from the binary about how much of the code was inlined from STL headers?
If you compile with debugging info, this info exists already. It's put there so that the debugger knows what source to show you when the code breaks on a particular instruction.
I'm working on a tool right now that would offer this information.
Exceptions != normal program flow.
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).
to have correct behaviour, all error cases need to have defined recovery. in this, error-return === exceptions.
The whole point is to never run in a broken state.... that path leads to expensive misery.
The point of resetting is to clear out bad state... if the condition has no defined recovery process then you cannot continue safely.
I'm not saying to not handle an error... I'm saying to not attempt recovery from undefined errors.
If you don't know what went wrong, don't attempt to 'handle' it... reset.
What is needed is true error recovery thought, not syntax that encourages proper to just put in empty exception handlers.
Your point is arguing for error returns in my view.
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
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?)
What Java lacks is a) enough compile time abstraction capabilities to inspect and parametrize exception specifications so that application specific exceptions can be forwarded through generic/framework code and b) a way to defer the checking to runtime (possibly via a special annotation) so that there would be no reason to ever write empty catch blocks.
Put differently: without exceptions, each function in your call stack is responsible for error handling of its callees, and for returning the correct error information to its caller. With exceptions, you only need error handling at the catch site.
This is about embedded systems. quite often there is no user and no display (for status purposes).
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.
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).
Can you show me an embedded system with zero cost exceptions?
$ 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)
$