> Returning to an alternate address shouldn’t be significantly more expensive than returning to the default address, so this has to be cheap. Modern CPUs add complications to arguments like this. Branches stall the execution pipeline, so branch prediction was invented to keep the pipeline flowing. Return instructions are perfectly predicted, which makes them literally free. At the very least, any alternate return sch…
This doesn't really make sense. The branch predictor relies on a history of previous executions of that branch, or on explicit hints, to decide if a branch will be taken or not. Based on this prediction, the speculative execution hardware then sees the jump (return/panic) and loads the code from that address into the icache. There is 0 difference between `if (condition) jump $panic_recover_address` and `if (condition…
Bringing Faster Exceptions to Rust
11–20 of 35 posts
Re: Bringing Faster Exceptions to Rust
#12Earlier quoted context omitted.
On x86, ret and call are explicit instructions. Ret always predicts the address of the last call, which is (usually) 100% accurate. Your example of `if (condition) jump $panic_recover_address` contains two branches, either of which can be mispredicted.
Sine Intel processors have shadow (call-)stack to ensure control-flow integrity, I imagine they use it to predict the return address as well.
The RSB has 10 or so entries and it is in the critical path, while the integrity stack might be larger and have less strict performance characteristics, so they might be separate objects.
Re: Bringing Faster Exceptions to Rust
#13Well, unwinding can be as simple (and as fast) as MOV SP, [installed_handler] JMP [installed_handler+WORD] but it only works if you don't need to run the defers/Drop's/destructors/etc. for stuff that's on the stack between the current frame and the handler's frame. Which you do, most of the time.
Re: Bringing Faster Exceptions to Rust
#14Well, unwinding can be as simple (and as fast) as MOV SP, [installed_handler] JMP [installed_handler+WORD] but it only works if you don't need to run the defers/Drop's/destructors/etc. for stuff that's on the stack between the current frame and the handler's frame. Which you do, most of the time.
Indeed. And the per frame cleanup is also language agnostic which adds overhead; it also must support both sjlj and dwarf frames[1]; it is also done in two phases: destructors are only run if an actual catch is found: an unhandled exception doesn't run destructors to preserve state in the core file. This requires a two-phase unwinding that again slows things down.
Another big bottleneck that might not be captured in OP test is that the unwinder has to take (or used to, things got better recently) a global lock to prevent races with dlclose, which greatly limit scalability of exception handling.
Still very nice improvements from OP.
[1] although I'm not sure you can mix them in the same program or it is a platform-wide decision.
Re: Bringing Faster Exceptions to Rust
#15Earlier quoted context omitted.
No, an unconditional jump to a fixed address can't be mispredicted.
How can a reusable function have a fixed return address?
Re: Bringing Faster Exceptions to Rust
#16> Returning to an alternate address shouldn’t be significantly more expensive than returning to the default address, so this has to be cheap. Modern CPUs add complications to arguments like this. Branches stall the execution pipeline, so branch prediction was invented to keep the pipeline flowing. Return instructions are perfectly predicted, which makes them literally free. At the very least, any alternate return sch…
As long as you don't overwrite the return address on the stack.
Re: Bringing Faster Exceptions to Rust
#17Earlier quoted context omitted.
On x86, ret and call are explicit instructions. Ret always predicts the address of the last call, which is (usually) 100% accurate. Your example of `if (condition) jump $panic_recover_address` contains two branches, either of which can be mispredicted.
No, an unconditional jump to a fixed address can't be mispredicted.
Re: Bringing Faster Exceptions to Rust
#18Well, unwinding can be as simple (and as fast) as MOV SP, [installed_handler] JMP [installed_handler+WORD] but it only works if you don't need to run the defers/Drop's/destructors/etc. for stuff that's on the stack between the current frame and the handler's frame. Which you do, most of the time.
> it only works if you don't need to run the defers/Drop's/destructors/etc Indeed. And the per frame cleanup is also language agnostic which adds overhead; it also must support both sjlj and dwarf frames[1]; it is also done in two phases: destructors are only run if an actual catch is found: an unhandled exception doesn't run destructors to preserve state in the core file. This requires a two-phase unwinding that aga…
If someone from another thread decided to unload a library whose code is still being executed in this thread then this thread would normally crash anyhow, and do so irrecoverably, right?
Re: Bringing Faster Exceptions to Rust
#19> Returning to an alternate address shouldn’t be significantly more expensive than returning to the default address, so this has to be cheap. Modern CPUs add complications to arguments like this. Branches stall the execution pipeline, so branch prediction was invented to keep the pipeline flowing. Return instructions are perfectly predicted, which makes them literally free. At the very least, any alternate return sch…
This doesn't really make sense. The branch predictor relies on a history of previous executions of that branch, or on explicit hints, to decide if a branch will be taken or not. Based on this prediction, the speculative execution hardware then sees the jump (return/panic) and loads the code from that address into the icache. There is 0 difference between `if (condition) jump $panic_recover_address` and `if (condition…
Re: Bringing Faster Exceptions to Rust
#20Earlier quoted context omitted.
> it only works if you don't need to run the defers/Drop's/destructors/etc Indeed. And the per frame cleanup is also language agnostic which adds overhead; it also must support both sjlj and dwarf frames[1]; it is also done in two phases: destructors are only run if an actual catch is found: an unhandled exception doesn't run destructors to preserve state in the core file. This requires a two-phase unwinding that aga…
> the unwinder has to take (or used to, things got better recently) a global lock to prevent races with dlclose If someone from another thread decided to unload a library whose code is still being executed in this thread then this thread would normally crash anyhow, and do so irrecoverably, right?
I think the mutex was mostly removed recently, by observing exactly what you mention: any race would be UB anyway.