Python 3.11 uses what is known as "zero-cost" exception handling.
Prior to 3.11, exceptions were handled by a runtime stack of "blocks".
In zero-cost exception handling, the cost of supporting exceptions is minimized.
In the common case (where no exception is raised) the cost is reduced
to zero (or close to zero).
The cost of raising an exception is increased, but not by much.
The following code:
def f():
try:
g(0)
except:
return "fail"
compiles as follows in 3.10:
2 0 SETUP_FINALLY 7 (to 16)
3 2 LOAD_GLOBAL 0 (g)
4 LOAD_CONST 1 (0)
6 CALL_FUNCTION 1
8 POP_TOP
10 POP_BLOCK
12 LOAD_CONST 0 (None)
14 RETURN_VALUE
4 >> 16 POP_TOP
18 POP_TOP
20 POP_TOP
5 22 POP_EXCEPT
24 LOAD_CONST 3 ('fail')
26 RETURN_VALUE
Note the explicit instructions to push and pop from the "block" stack:
SETUP_FINALLY and POP_BLOCK.
In 3.11, the SETUP_FINALLY and POP_BLOCK are eliminated, replaced with
a table to determine where to jump to when an exception is raised.
2 0 NOP
3 2 LOAD_GLOBAL 0 (g)
4 LOAD_CONST 1 (0)
6 CALL_FUNCTION 1
8 POP_TOP
10 LOAD_CONST 0 (None)
12 RETURN_VALUE
>> 14 PUSH_EXC_INFO
4 16 POP_TOP
18 POP_TOP
20 POP_TOP
5 22 POP_EXCEPT
24 LOAD_CONST 2 ('fail')
26 RETURN_VALUE
>> 28 POP_EXCEPT_AND_RERAISE
ExceptionTable:
2 to 8 -> 14 [0]
14 to 20 -> 28 [3] lasti
(Note this code is from an early 3.11 alpha, the NOP may well have be removed before release).
If an instruction raises an exception then its offset is used to find the target to jump to.
For example, the CALL_FUNCTION at offset 6, falls into the range 2 to 8.
So, if g() raises an exception, then control jumps to offset 14.
Unwinding
---------
When an exception is raised, the current instruction offset is used to find following:
target to jump to, stack depth, and 'lasti', which determines whether the instruction
offset of the raising instruction should be pushed.
This information is stored in the exception table, described below.
If there is no relevant entry, the exception bubbles up to the caller.
If there is an entry, then:
1. pop values from the stack until it matches the stack depth for the handler,
2. if 'lasti' is true, then push the offset that the exception was raised at.
3. push the exception to the stack as three values: traceback, value, type,
4. jump to the target offset and resume execution.
Format of the exception table
-----------------------------
Conceptually, the exception table consists of a sequence of 5-tuples:
1. start-offset (inclusive)
2. end-offset (exclusive)
3. target
4. stack-depth
5. push-lasti (boolean)
All offsets and lengths are in instructions, not bytes.
We want the format to be compact, but quickly searchable.
For it to be compact, it needs to have variable sized entries so that we can store common (small) offsets compactly, but handle large offsets if needed.
For it to be searchable quickly, we need to support binary search giving us log(n) performance in all cases.