4 ms·
So. The proposed mechanism is: 1. Every function now receives an additional parameter that is basically a pointer to a std::optional<exception_t> (from now on,
by Joker_vD 7y ago
So. The proposed mechanism is:
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.
- blattimwind 7y agoThis is pretty much how exceptions are implemented in some bytecode interpreters (e.g. CPython).
- aw1621107 7y agoIs that an ABI break due to the extra parameter? I skimmed the paper, but it only seemed to talk about the standard table-based unwind in passing, with no mention of compatibility. Did I miss something? 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.
- gpderetta 7y agoIt is ABI breaking but there is a way out. Instead of replacing the existing exceptions, this mechanism could be used for checked exceptions only which will necessarily require annotating functions, so ABI breaks are to be expected.
- aw1621107 7y agoWouldn't function annotations be an API break, too? And I'd imagine checked exceptions would get a decent amount of pushback due to Java's implementation.
- gpderetta 7y agoYes, but they are opt-in. I think, given the right abstraction capabilities they might work in C++. They are semantically no different than returning an either<T, error>.
- ctz 7y agoIs there an example where changing the exception handling system does not change the ABI?
- twic 7y agoChanging the handling of existing exceptions, no. But adding a new kind of exception is fine. That's what the herbceptions proposal did. However, i think they are in fact proposing to change the ABI here.
- aw1621107 7y agoI'm not familiar enough with the darker corners of C++ implementations to say, but I'd guess not. From what the paper describes it seems table-based exception handling is deeply ingrained into the Itanium ABI so any significant change would be an ABI break, which would make said change more difficult to spread.
- 7y ago
- brandmeyer 7y agoHonestly, it looks like someone is just taking the LLVM IR instruction `invoke` literally.
- brandmeyer 7y ago> Somehow, it ends up with the same performance despite all of this constant branching error-checking and better performance when re-throwing exceptions. 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.
- gumby 7y agoReal-time != fast.
- aw1621107 7y agoFor "standard" applications, sure, but the paper mentions that for embedded uses code size and determinism can be more important than pure execution speed, and perhaps a few percent slowdown would be considered acceptable for embedded development if it allows the use of exceptions.
- Joker_vD 7y agoPerhaps. But 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?
- aw1621107 7y ago> 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 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>.