3 ms·
In C++ with the Itanium ABI, exceptions only add overhead when thrown, not on every function call. In Rust, the result of every function call is checked.
by softc 10y ago
In C++ with the Itanium ABI, exceptions only add overhead when thrown, not on every function call. In Rust, the result of every function call is checked.
- akiselev 10y agoI see, my knowledge of exceptions is quite out of date then. Regardless, both of those work out to the same performance because Rust's single compare will be optimized away by the branch predictor except when errors occur. In the error case, C++ exceptions would have a lot more overhead that you would have to explicitly opt into in Rust on a case by case basis (so no need to disable errors altogether).
- softc 10y agoNot every platform has a branch predictor, like low-cost embedded platforms, where bare metal languages like C++ are commonly used. Error-case performance is negligible as it's much more rare (cf. Amdahl's law). If errors are common in a specific section of code, in C++ you have the option of using simple error-checking. You have no such counter-option in Rust.
- akiselev 10y ago> Not every platform has a branch predictor, like low-cost embedded platforms, where bare metal languages like C++ are commonly used. You're moving the goal posts. Itanium processors do have branch prediction so comparing C++ exceptions on Itanium to Rust on embedded architectures is meaningless. In embedded Rust, the worst case scenario is a compare between a constant and a register or a byte on the stack. Most non-ARM embedded platforms I've written firmware for barely have a three cycle multiply so a one cycle compare will be negligible except in the most extreme of cases (where you'd just write inline assembly anyway, no matter the language). On those platforms, you spend more time restoring registers than comparing the result of a call. On ARM processors, conditional execution behaves like a branch predictor on short compare jumps so there's again no overhead (except with value types, in which case Option adds 1-4 bytes for the None case on the stack). Either way, Rust's error checking overhead is irrelevant. If your embedded environment is so resource constrained that Rust's error checking is too much overhead for you, then you certainly can't use C++ exceptions. They are too unpredictable without prohibitively expensive physical testing and tedious tuning of worst case code paths (unless you don't care about hard real time guarantees at all). If it's not, then it doesn't matter whether you use Rust errors or C++ exceptions because the overhead is a rounding error. We're not talking about something like reading memory from cache vs RAM or static vs dynamic dispatch where the difference in performance can be 10-1000x, we're literally talking about single digit cycles here. > If errors are common in a specific section of code, in C++ you have the option of using simple error-checking. You have no such counter-option in Rust. Yes, you do. You can unwind the stack manually and jump to the error handler, just like the C++ compiler does for you with exceptions. This is how the panic macro is implemented so all you have to do is modify its implementation (compiler-builtins crate) and create your own catch_unwind function. With the amount of time you'd spend typing "if (result != null)" or implementing an option type in C++, you can write your own exception implementation using Rust macros and traits. Rust developers made the right decision: "simple" error checking is the default with syntactic sugar to ease its use and if you really need those few extra cycles, you can make your own crate or compiler plugin that will give you the same features without polluting the rest of the ecosystem with a default error handling mechanism that is opaque, complex, and unsuitable for resource constrained real time systems.
- softc 10y agoThe Itanium ABI is basically the reference ABI for all platforms, it doesn't only apply to Itanium. Rust's error checking overhead is not unanimously irrelevant, it's not uncommon for functions to be called in tight loops. I won't address your other comments since they are subjective and/or dependent on circumstances.
- Gankro 10y agoAgreed on this, but this is ignoring the much more subtle effect of unwinding: it introduces optimization barriers around every function that "might" throw. As a basic example: x = 1 something_that_can_fail() x = 2 With unwinding, the `x = 1` write must be preserved if whoever catches can observe it (and if that's unclear, it must be preserved conservatively). The C++ STL riddled with complex machinery to try to work around this sort of thing because everything can throw. Copies, moves, you name it! Not even reallocating an array can be done with realloc unless non-throwingness can be proven. If you try to make throwing "opt in" like Java, then you end up with nasty "throwingness polymorphism" problems and end up with constructs like Swift's "rethrows". By contrast, error propagation through normal values naturally composes because it's really easy to write code that generically handles return values.
- softc 10y agoGood point. Some responses: Function calls themselves can force something like "x = 1" if x is observable from the callee. In the case where "x = 1" is not observable from the catch block, then there is no effect. In cases where you would use "try!" in Rust, in the equivalent C++ cases the catch block wouldn't be able see local variables since it would be in a parent function call. I think LTO largely eliminates this issue altogether since function bodies are visible to the optimizer in that case.