5 ms·
> You get an error state added to a given value, which you also get via exceptions, except implicitly and without notification of the additional state. This is
by kllrnohj 4y ago
> You get an error state added to a given value, which you also get via exceptions, except implicitly and without notification of the additional state.
This is incorrect. With error return values you're adding a branch to every function call which is quite expensive on the whole. You're adding i-cache pressure & you're adding branch prediction pressure.
Exceptions in nearly every language that supports them (including C++) don't go through return values at all. Rather when thrown the stack is walked to find an exception handler. So exceptions are more expensive to throw than return values, but completely free when not thrown unlike return values.
> It very much isn't, part of the system programming workload is to provide reusable components.
There's absolutely no issue with exceptions & library boundaries in general. Statically checked exceptions also exist (see Java - although there's a big debate on if that's a good idea or not, but also see https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0709r3.pdf https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p07... ), I'm not sure why you're arguing as if they don't.
- worik 4y ago> So exceptions are more expensive to throw than return values, but completely free when not thrown unlike return values Is that true though? Placing exception handlers in the stack, so examining every stack frame for one, is equivalent to testing the sum type to see if it is in error state, surely? My disclaimer: I know very little about compilers, so this is an actual question.
- mgaunard 4y agoYou don't put exceptions handlers on every frame.
- worik 4y agoAs the stack unrolls where does it know to find the handlers? It must (?) unroll the stack
- mgaunard 4y agoYou mean destructors? An exception handler would be a catch block. Anyway, the typical implementation involves two phases, one which uses a table to identify the matxhing catch clause, then another one going through landing pads for each frame of the stack. Just consult the Itanium ABI spec for technical details.
- kllrnohj 4y agoThat's only when an exception is thrown, though. If an exception isn't thrown, "unroll the stack" is just a normal `ret` instruction. There's no exception handling code at all when a function returns normally without an exception, which is the point. By contrast when an error sum type returns without an error, you're still doing a branch at the call site to verify that. Here's a simple example showing exceptions vs. sum types: https://godbolt.org/z/9Er6eKxs9 https://godbolt.org/z/9Er6eKxs9 In the non-throwing exception path, there's literally no error or exception handling code executed at all. Whereas in the sum-type error-returning version, you have a branch at every call site that's always executed regardless of if there's an error or not. Now the exception handler generates ".cold" clones of the function, so the total assembly for the exception handling one is larger. However, that assembly isn't every executed if an exception isn't thrown, which is the broader point. So it's not taking up CPU cache space & it's not taking up branch predictor slots.
- worik 4y agoThe point is there is no advantage to throwing an exception VS. using sum type return values (e.g. Result types in Rust)
- kllrnohj 4y agoThat's a bad point? Exceptions are not normal control flow. They are rare or, as one might say, exceptional. The performance of them when thrown isn't of key concern, it's the performance when they are not thrown that matters since that's the >90% case. And in that case, code using exceptions is faster than code using sum type return values, especially if those errors propagate deeply across the call stack which they very often do.
- inkyoto 4y ago> So exceptions […] completely free when not thrown unlike return values. In C++, they are not. Since C++ allows objects to be created on the stack within the each current scope, every constructor call for such a new object has to register its destructor with the exception handler's object cleanup jump tables. Consider an edge case with a loop where 100k local objects are created. Each constructor invocation will incur an overhead of an indexed memory store of the destructor's address in the exception handler's table[0] for each object. An indexed memory store is typically 1x instruction for a CISC architecture (it can be more if the data section is located too «far» in the address space, and the ISA limits the offset width in the instruction encoding). It is typically several load low and shift, load low and store instructions on a RISC architecture. L1 D-cache and L2 cache, sometimes L3 cache as well (if the exception table grows large), and TLB reloads[1] get involved at all times. All of the aforementioned is just to register a destructor for an exception that might not occur. So, no, it is not free and (occasionally) the time dimension is not even clearly defined. This is rather unique and specific to C++ because it is an outlier and allows new objects to be created on the heap AND also on the local stack. Languages that allow new objects to be only created on the heap do not incur such an overhead. [0] Bonus point: 100k local objects being created inside a try/catch block will also blow the jump table out of proportions and add more cache pressure and cache line reloads. [1] And even page faults might occur – if the exception table crosses memory pages.
- zugi 4y agoCalling destructors on scope exit is a C++ language feature with or without exceptions. I'm no expert but I believe these addresses can be determined relative to the stack frame pointer, so they don't need to be registered in advance. Instead extra code is created to call all those destructors; this code is only ever called if an exception is thrown. That results larger executables, but no extra CPU instructions are executed unless an exception is actually thrown. (In theory a compiler-writer could try to reuse destructor-calling code between the normal exit case and the exception case, but that might force one extra branch in the non-exception case.)