Low-cost Deterministic C++ Exceptions for Embedded Systems (2019) [pdf]
research.ed.ac.uk
research.ed.ac.uk
1. Every function now receives an additional parameter that is basically a pointer to a std::optional<exception_t> (from now on, "exception holder"; exception_t can hold any value whatsoever).
2. "try" is translated into allocating a (new) exception holder on the stack and passing pointer to it in all function calls inside of the try block. "catch" is translated into checking this exception holder for holding an (appropriate) exception. If "yes", handle it, if "no", go on.
3. Throwing an exception is done by putting the constructed exception into the exception holder passed to you, and returning as usual.
4. Each function call is now followed by checking whether the exception holder holds an exception. If "yes", return immediately, if "no", keep executing.
Example:
void C(std::optional<exception_t>* exc_holder) {
Bar bar;
exc_holder->emplace(0);
}
void B(std::optional<exception_t>* exc_holder) {
Foo foo;
C(exc_holder);
if (*exc_holder) { return; }
}
void A(std::optional<exception_t>* exc_holder) {
std::optional<exception_t> new_exc_holder;
B(&new_exc_holder);
if (new_exc_holder)
if (new_exc_holder.value().is<int&>()) {
int& p = new_exc_holder.value().as<int&>();
// catch body...
} else {
*exc_holder = std::move(new_exc_holder);
return;
}
}
}
So, it's basically "if err != nil { return <default_value>, err }" but with slightly less amount of copying error values. Somehow, it ends up with the same performance despite all of this constant branching error-checking and better performance when re-throwing exceptions.If it is in fact an ABI break, how much of an obstacle would that be to getting this through the committee? I was also under the impression that up-to-date compilers/runtimes are not something one can take for granted in the embedded world, so recompiling everything is a shaky proposition.
Did Microsoft release a paper with that conclusion? I'd be curious to read more about their reasoning.
> Although it was initially developed for the Itanium architecture, it is not platform-specific and can be layered portably on top of an arbitrary C ABI.
My mistake, my comment is mostly rubbish then.
In my understanding:
Historically, the committee basically said "if your paper breaks the ABI, it won't be considered, don't bother." In Prague last month, the committee voted that C++23 would not break ABI, though that decision is not final. However, they did say that authors should bring papers that would break the ABI, and they will be considered in a general sense.
So, it is now possible, at least.
By the way, I know Rust doesn't guarantee a stable ABI, but has Rust gotten significant improvements out of not making that guarantee?
We have done some breaking stuff too, and that has been useful, but in my mind, that’s less important.
And I'd imagine checked exceptions would get a decent amount of pushback due to Java's implementation.
I think, given the right abstraction capabilities they might work in C++. They are semantically no different than returning an either<T, error>.
SJLJ exceptions were also deterministic and slow.
The author's recorded a 2-4% slowdown in the case that exceptions weren't thrown (relative to zero-cost DWARF EH), and a 1-2% slowdown in the case that exceptions were thrown (again, relative to DWARF EH).
That's not an improvement, that's a regression.
a) apparently branch prediction is good enough to make all this constant checking for exceptional returns to slow down the execution by only a couple of percents;
b) apparently exception propagation and re-throwing is way, way faster than in the standard implementation;
c) the exclusion of the unwind tables makes the code 3 times smaller on ARM, and 5 times smaller on x86-64. That's pretty huge.
So all in all, I'd say that if those figures actually hold, then this approach is pretty solid. It's just surprising, given how much complaining there is about how Go's idiom of "if err != nil { return nil, err }", or Rust's "let v = some_fun()?" kills performance because of branch mis-prediction and hot path code bloat, so maybe those figures don't actually hold?
Was that the main complaint? I thought that Go's thing was was more about being noisy boilerplate and a bug magnet rather than a performance issue. Not sure about Rust, either, since IIRC the ? operator is sugar for the try! macro, which itself was just sugar for bubbling up a Result<Err>.
In the end, I still use exceptions, so this gives me hope. I like C++ exceptions as a feature, but I'm not happy about how it's implemented right now.
I wrote some tests, not sure if they are perfect:
#include <stdexcept>
volatile int i = 1;
__attribute__((noinline)) int test() {
return i;
}
int main()
{
if (test() == 0) throw std::runtime_error("");
return 0;
}
====>
Instruction count: 612
Memory usage: 116 KB, top: 116 KB
Compile time: 745326 micros
Execution time: 149 micros
Binary size: 98 KBThe above is with exception, below is without:
volatile int i = 1;
__attribute__((noinline)) int test() {
return i;
}
int main()
{
if (test() == 0) return -1;
return 0;
}
====>
Instruction count: 251
Memory usage: 16 KB, top: 16 KB
Compile time: 441626 micros
Execution time: 53 micros
Binary size: 3 KBJust remember to pick newlib as the target, as that is the thinnest, and the no-exceptions variant will be only 16kb.
Also, what are your compile flags? godbolt shows a pretty big difference with -O2.
I agree completely, it really does run everything from start to finish. Sometimes that's what you want, and sometimes you don't. I could break on main as an option, and then start the benchmarking after main. It would include teardown, unless you call _exit() at the end of main.
The compile flags are -O2, no linker GC.
The alternative to having the exception-handling code out of line is, of course, handling all exceptional conditions in line, which not only makes the code harder to read but makes the generated object code bigger and, if it fails to fit in a cache line or causes unwholesomely large numbers of branch evaluations (see spectre and meltdown) results in slower or more insecure code. The real answer to C++ exceptions is the classic C-style programming where all errors are just ignored.
Of course, no amount of reasoning or hard data survives in the face of religious belief, and 80% of programmers out there are part of one cargo cult or another, so carry on with what you were doing. It's probably good enough.
That's a mischaracterization of most C code written by professionals, because (with appropriate compiler flags) return codes have to be explicitly ignored. Contrast with languages where most functions don't even have a return code, and the compiler/interpreter doesn't complain about failing to catch possible exceptions. I see this a lot in Python code (which I generally like and have liked since 1.5 BTW), a bit less so in C++, etc. Even "elite" programmers in those languages tend to be sloppy that way, which I rarely see in C programmers with more than a couple of years' experience.
I've been a professional programmer for 40 years, much of that in C. I've seen a lot of code, some of it in production in critical systems for many years. It is not a mischaracterization, it is a description of the state of the art.
No, Python's default behavior is an uncaught exception causing program termination with a stack trace, which is not generally what anyone wants. You have to add code to get non-default behavior.
> A stack of ten function calls should not have ten try blocks
While I agree with that, it's kind of missing the point. Exceptions can be used well. So can return codes. The problem is that when letting an error/exception be "someone else's problem" is the default, that tends to be what everyone does. It becomes nobody's problem, except the user who's left staring at an inscrutable program-termination message. It's the Volunteers' Dilemma in code.
A good error-handling paradigm would require that errors be explicitly handled, passed on, or suppressed. Exceptions let them be invisible. C-style error returns are a bit cumbersome, but still better for correctness. My favorite approach right now is Zig's, based on error returns but with extra features (e.g. defer and try) to make common idioms less cumbersome. I've heard Rust is similar, but haven't really looked into it.
We narrowed it down to the fact that the compiler wasn't free to A) inline the function, B) reorder internal operations in the function which came before vs. after the exception being thrown.
IIRC that was the originally proposed behavior for noexcept, but it was changed at some point to call std::terminate instead.
However, I don't find any referenc to noexcept having gotten defined behaviour. Do you have any pointers?
> Whenever an exception is thrown and the search for a handler (14.4) encounters the outermost block of a function with a non-throwing exception specification, the function std::terminate is called (14.6.1).
I don't know when precisely noexcept was changed to call std::terminate, but I did find N3103 (included in the 2010-08 post-Rapperswil mailing [1]), which argued that the standard should require calling std::terminate to avoid security issues.
[0]: https://github.com/cplusplus/draft [1]: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2010/n310...
I haven't measured the exact effect of this, but - although that if the wind doesn't throw the ship doesn't slow - having an exception-al path through the code will stop some compiler optimizations. I have seen this first hand but I also remember Walter Bright mentioning it either here or elsewhere.
If this is definitely the case maybe someone could chime in why exactly (I've never fiddled with a backend in the right places to deal with exceptions)
In your opinion, what are situations/examples that justify the use of exceptions?
I haven't used a language with exceptions since 3 years (i.e: since I switched to Go), and I haven't yet faced a situation where I felt a need for them.
Also, I started to learn C++ as a hobby a few months ago, and I have to say that I still don't understand when I should use exceptions or not, things seem fine so far by just returning some error values.
https://ninenines.eu/articles/dont-let-it-crash/
Basically this approach is handle errors if you know what to do with them, otherwise leave the error for something else.
In erlang, where you have one process per transaction, and nothing handles the error, the process crashes.
In C++, you could just have an exception handler around a transaction, and then when an exception reaches that point you throw away the transaction, but log that it failed, you could have exception handlers below this to try and do rollbacks etc.
This is overly simplified but it gives a basic idea.
This general approach of «let it "crash"[1]» or happy path programming is what I used when I wrote OTLP systems for mobile networks which were performing financial transactions. The exception handling would allow you to write fairly complex business logic and relying on exception handling to recover to a stable state.
[1]: Crashing in Erlang terms is not at all what people generally mean with crashing. It is a very unfortunate word because in part many of the problems in node.js comes from joyent not understanding what "let it crash" meant - it did not mean let the unix process crash. Erlang does one process per transaction and they are incredibly lightweight. Some details from joyent can be found here: https://www.joyent.com/node-js/production/design/errors
In my experience, the amount of interesting logic that "something else" does is vanishingly small. An arbitrary real-world example probably just logs or returns a string that is opaque and mostly useless to any potential error recovery mechanism.
In theory one could throw a FileExistsException with semantic and accessible data like filePath, but as things get this specific, they practically become part of the API. And a leaky part at that. The top-level entities that need to handle the exception need to know, somehow, that (1) encapsulated implementation details may throw something specific and (2) what to do with the thrown events.
So, practically, programs tend to catch std::exception, and pass the opaque error string on to some form of external I/O system (logs) with some minimally informative error status code (ERROR, 5XX, -1, false, null).
It is interesting to perform transaction failure operations, but there is no std::transaction_aborted, std::connection_dropped, or std::resource_busy. More realistically, std::exception is caught, which could just as easily be std::bad_alloc or std::domain_error. What do you do with a domain_error? I don't know either.
At any rate, the most interesting feature for transactional logic is RAII semantics, and you don't need to use exceptions to leverage those.
> They are errors but trying to handle every little bit of how a computer can go wrong is not really a feasible task, although you might be inclined to handle some.
Though, using error values, it's not that different.
1. check if the error is something that you care about
2. if not, check if it is something that you should pass back to the caller (in general that's an assert "if not null/empty"), if it is then return it
3. otherwise continue what you're doing in the current function
You have the same result without a need for exception, you only handle what you care about in the current scope, or let the chain of callers decide what they want to do with the error.
For example, in a user-facing desktop application, maybe you just catch the exception in a high-level place, log the error, report it to the user, and explain that the action they just attempted failed. They can then try again, without having to restart the entire application.
[1] Many types of network servers, for example a multimedia streaming server or a SOCKS proxy, only need to allocate dynamic memory during the early phase of the connection. After that the client can be served indefinitely. If you're writing a library, best practice is to assume your caller can handle malloc failure; if not they can choose to abort.
In modern C++ this is already the case. For example, every container that implements an at() function will throw an exception if the (unsigned) index is >= size.
If you look at the assembly of a simple function only accessing a std:array using the [] operator, you will see that it's quite small. If you access it using at() you might see that the core function remains the same, except it now has a bounds-checking test which jumps to a huge postamble. The postamble will throw the exception. Since the CPU likes to execute instructions in sequence, we say that the happy path (which is indexing correctly) is now the fast path.
For example, since constructors can't return error values reasonably, you would have to make them all private and use static factory functions, or similar workarounds. It's certainly possible, but much less pleasant to work with.
How do people deal with constructor errors in the C++ world? I have never seen C++ code where the constructor call is wrapped in with an exception handler. I would guess that's done by creation an addition function "make_my_object"? But then what is its signature? Or is an exception handler used in that case?
There are about 200 different ways to be honest, stemming from the unfortunate situation that the most common advice, "use exceptions in exceptional situations", is actively harmful.
Let's take a "Connection" class for example. You'd initialize it with an IpEndpoint, or something to that effect. Some people think "well I'm writing a download script so if I can't connect the script can't continue, so that is exceptional" - so they throw an exception in the constructor and catch it somewhere in main. Other people are writing something more complicated and they think "being unable to establish a connection is normal, I'll just make a factory function for this and wrap the Connection constructor". Which leads to completely different paradigms in the same situation but with different environments - and those two decisions are both good, there are tons of decisions that aren't!
This is compounded by the fact that the C++ standard library only uses 2 kinds of resources: Memory and Files. And they use different kinds of error handling. Well, memory allocation always throws (at least it's consistent here), but the iostream error handling is, uh, interesting? Streams don't throw by default, however you can enable them to throw at runtime, but they also store their error state inside. It's a mess really.
Oh and also the std::logic_error exists and gets thrown by standard library functions, but a lot of advice around exceptions says that you're not supposed to use exceptions for programming errors, that should be handled by assertions.
And then there are of course people who just disable exceptions. Some disable exceptions but still use the standard library and just kind of hope that everything will still work?
So yeah, it's a mess. That's partly why proposals like this would help - if you can make the actual throwing of exceptions cheap, the advice of "use exceptions for exceptional situations" goes away instead becomes "use exceptions for every kind of error handling". Which would be much more consistent. And people wouldn't have an excuse to disable exceptions anymore, at least in the long run.
No, there are features in most of the standard library that allow you to confirm that an object is in a state where it won't throw an exception, and you use those. For example, with std::map you can check the return of find() against end() to determine whether you can safely access the second element of the tuple. So you would do that before you access the tuple.
And you would generally write your own classes to either crash the process quickly or fail gracefully on construction. Basically you have to manage the shutdown of your process when you encounter exceptional situations in the constructor.
You can also count on any "exception" that is thrown with no-exceptions to result in an immediate crash.
It's not like the errors are just ignored then. The program just aborts instead of unwinding stacks. And all the APIs you care to use have defensive ways to use them. For instance, you can check if a vector is empty before accessing element zero.
The point of exceptions is not to wrap every call that can throw but instead to let them bubble up to have them be handled where it makes most sense - a large amount of software only needs to show an error dialog to the user and asks the user to save / exit, or log a message somewhere (so a single `catch` at the top of your event loop).
Only when you know for a fact that a subsystem can actually handle a specific case of error more intelligently, then you add exception handling there.
I've got some ideas for siphoning off the errors to make them out of bound. Will need to experiment to see how ergonomic it is.
I do a lot of stuff in embedded, and it would be nice to have something like this. I'm also experimenting with using C++ as a compiled scripting language for a game engine, and for that I would like the binaries to be really small.
Because of docker, compiling freestanding binaries for any architecture is not such a barrier anymore.
You can't do anything similar for exceptions.
in an extremely narrow field. Most if not all the apps running on my desktop right now are written in C++ and I'm pretty sure exactly zero do that.
I taught a bit at uni and you can't imagine the amount of students who read stuff like parent comment's "it's common to not use malloc" on internet and then try to apply this as a mantra to their codebase when the assignment is to make a freakin snake game with SDL and write actually legible code - the immense majority of them will never ever encounter any realtime problem and don't need to think that this is an actually common thing - it is not !
This is doing a disservice to the whole profession. If you actually need to program real-time systems, you will have processes in place to ensure that things don't end up in a syscall - or just don't have malloc at all available in which case the problem does not exist.
None of this is operates in any way remotely close to "malloc up front and never touch again." Nor do they operate with any sort of significant constraints vs. the typical app.
There are definitely things that do that. But even in the world of embedded it's a subset of things that do that.
Heck, the most software-constrained thing I have is probably the controller in my monitor. But then again that also runs Altera Arria V GX FPGA with 768MB of DDR3 RAM, which also definitely classifies as a niche area with highly specialized demands.
It is not common over the population of Facebook users.
One is that the effective behavior of a function can be changed with no change to the function itself because of an exception thrown "over its head" from something it calls to something that called it. This makes both human and machine analysis significantly more difficult.
The other, somewhat related, has to do with performance. It's easy to do a function-by-function analysis of runtime in error and non-error cases, and those analyses are readily composable. With exceptions, it's much harder to predict how much time will be spent unwinding through M functions, calling M*N destructors, etc. This is exacerbated by the fact that exception-oriented languages also tend to encourage idioms that involve many more possibly-expensive object creations/deletions (including the exception object itself). "Zero cost abstraction" is true much less often than people seem to think.
If you're concerned with ensuring that a given piece of code will never exceed a certain execution-time bound, your job becomes at least an order of magnitude harder with exceptions than with plain old return codes, even if the two seem equivalent at some higher conceptual level.
Given that the C++ standard throw exceptions in many places, and is only adding more in future versions, all this needs checked anyways. So this argument is a red herring. Proper exception handling is not optional for robust C++ code, since anyone under you can throw when someone makes feature changes or uses new features that throw in the future.
>your job becomes at least an order of magnitude harder with exceptions than with plain old return codes
No it doesn't. I routinely write high performance C++ code, often using hand crafted assembly where needed. You learn what can throw, what cannot, and don't put exceptions in the hot path.
How often are you really writing crazy optimized code where anything exceptional can happen? I think never. No allocations, no files being pulled from under you, no using reousrces you don't have properly set up before you enter the hot path.
>calling M*N destructors
What does this mean? Are your objects in an object soup of interdependencies? Use proper system decomposition and statements like this would never make sense.
> harder with exceptions than with plain old return codes
Suppose you have functions calling each other 10-20 deep, acorss a library or two, and the bottom guy in LibA returns an error code in the style of LibA. Somewhere up the stack a function using libA also touches LibB and your own stuff, all of which can error.
Now at the top, and along the entire path, you need to marshall error codes or change them, adding lots more code in the hot path, all checking and passing, and hoping someone doesn't make a change somewhere, because now you get to revisit every one of those paths to ensure new codes get transcribed....
The code is slower, has more branching, is harder to maintain (this adding bugs), than simply throwing when something fails at the bottom and handling it where appropriate.
So returns codes in fact make code messier, more error prone, and certainly larger, overflowing caches more often than simply using modern exceptions.
That puts you light-years ahead of most C++ programmers I've encountered.
> What does this mean? Are your objects in an object soup of interdependencies?
Not my code; other people's. We're talking about the typical programmer here, not the outliers.
> at the top, and along the entire path, you need to marshall error codes or change them....
Yeah, it's a pain, but that's totally not the point. Ease of verification and ease of later modification are orthogonal.
> The code is slower, has more branching, is harder to maintain (this adding bugs), than
"Simply handling..." How cute. What about proving that it's handled, not just sometimes but every time? What about the legions of programmers who "forget" to handle some exceptions? What about the dozens of aborted processes that I see every day while running one of the world's biggest storage services at a company famous for its coding interviews, that result from such sloppiness? Never saw so much garbage in the many projects I've worked on that didn't use exceptions.
Look, I'm not saying that return codes are all sunshine and roses. Neither are exceptions. I'm making a very specific point that exceptions make code harder to verify for either correctness or bounded performance, because somebody asked. Maybe you prefer exceptions and that's fine, but that's an advantage they don't have. These unfounded claims about non-exception code being so much slower or less maintainable (despite your own claim to write such code for high performance) add nothing to the conversation.
You're relying on the typical programmer to handle all error codes, pass them all throughout all the levels of an application, and not miss any, yet you don't want to use exception handling and train them to use that? Its's vastly less error prone.
>What about proving that it's handled, not just sometimes but every time?
Plenty of analysis tools do this such as Coverity. It's been done for literally decades.
Which is easier? Handling error codes each and every time, or ensuring that there are some top level exception catchers in important places, or simply one at the top, logging failures?
And in the previous section you were arguing about writing highly optimized code - this is not the domain of "typical programmers". Pick your goalposts.
>Yeah, it's a pain, but that's totally not the point.
If it's painful, it's less likely to get done. This is true for typical and advanced programmers. If it's painful, it's also more error prone.
>What about the legions of programmers who "forget" to handle some exceptions?
Simple to handle at a higher level, log and exit. Automates testing can easily catch them all for you, since you can have programs locate every throwable place in C++ and inject them.
Good luck automated testing all possible error codes, which are not easily programmatically discoverable.
You also didn't address that C++ already is full of places that throw exceptions, and more are added in each standard, and all foreseeable future language proposals expect them to be available and used.
So - if you have to learn how to use them simply to use C++, why not teach yourself and those around you best practices on using them? RAII, nothrow swaps, the three guarantees, etc.? It will make better, much more readable, and better extensible and maintainable code.
>These unfounded claims about non-exception code being so much slower
So you claim checking every level of code if something failed, only to pass it up higher, does not add code size (I.e., pushing out of cache) or branches (i.e., having to deal with branch prediction) in a hot path? This is pretty easy to see. There's plenty of places where a low level rare failure needs passed through several layers of code to a section that can do something about it, and an exception for that case is demonstrably, provable a faster hot path.
If you really need me to code one up and show you I can. It's trivial to check, and this is not a rare case. There's a reason modern compilers went to the zero overhead exception structures for not taken exceptions, and this is a prime example.
Having such huge mechanisms as optional programming language features is also problematic for collaboration on big project and for using third-party code that made different decision. I prefer when language designer is very opinionated on such decisions, and then the combination of all decisions either proves to be clumsy or gains wide following.
The approach is somewhat simple and really not that clever, yet the outcome looks impressive enough. This surprises me as it's just a mechanism of what people have been arguing over years.
Nicely done!
Interesting paper, but it's not gonna fly in practice.
> Sutter [19] has only very recently proposed re-using the existing function return mechanism in place of the traditional stack unwinding approaches, requiring no additional data or instructions to be stored, and little to no overhead in unwinding the stack. Furthermore, by removing stack unwinding’s runtime reliance on tables encoded in the program binary itself, the issue of time and spatial determinism is solved. As it is possible to determine the worst-case execution times for programs not using exceptions, it follows that exception implementations making use of the same return mechanism must also be deterministic in stack unwinding.
> Given these clear advantages, we have based our implementation on this design, with a core difference being the replacement of their use of registers with function parameters, allowing for much easier interoperability with C code, which can simply provide the parameter as necessary.
> A limitation with [19] is that they require all exceptions be of the same type, leaving much of the standard exception-handling functionality up to the user. Our novel approach includes a method of throwing and catching exceptions of arbitrary types (as with existing exception handling), without imposing any meaningful execution-time penalties when exceptions do not occur.
At least, it uses a comparable implementation strategy. Herbceptions used a different surface syntax, adding a new kind of exception and a new way to declare them.
I rather like that herbceptions are more restricted in type. Restricting the type means that a programmer catching an exception has more things they can do with it. It's currently impossible to sensibly handle exceptions of unknown type in C++. The change is an improvement, not a limitation.
The point about interoperation with C is interesting. How would that work? If you wanted to call a C++ function from C, would you have to kludge the prototype to add the exception pointer?
The herbceptions paper proposed trying to get the C ABI changed to support the same exception mechanism:
> 4.6.11 Wouldn’t it be good to coordinate this ABI extension with C (WG14)?
> Yes, and that is in progress. This paper proposes extending the C++ ABI with essentially an extended calling convention. Review feedback has pointed out that we have an even broader opportunity here to do something that helps C callers of C++ code, and helps C/C++ compatibility, if C were to pursue a compatible extension. One result would be the ability to throw exceptions from C++→C→C++ while being type-accurate (the exception’s type is preserved) and correct (C code can respond correctly because it understands it is an error, even if it may not understand the error’s type).
[1] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p070...