News for hackers. Stuff that matters.
Re:Python 3.11: “Zero cost” exception handling by bregma on Wednesday October 06, 2021 @09:55AM (#28773231)
Exceptions always have a cost. What happened with many C++ runtimes is that they moved the execution cost of exceptions almost entirely into the exception raising mechanism so that the path of execution that does not raise exceptions has no overhead due to exceptions. This was not the case with the Python runtime.

The "zero cost" implementation for C++ exceptions in the case of ELF binaries means storing a bunch of static (compile-time generated) data regarding CPU register state changes for each function in a table indexed by function address offset within the DSO. When a "throw" expression is executed, the execution stack is crawled and that table evaluated for each function encountered until an appropriate "landing pad" (eg. a matching "catch" clause) is found, and then the stack is unwound by interpreting the function table contents to step through the various CPU states in reverse order, executing destructors as required. It's just like sufficiently advanced technology. In the case of no "throw" expression, there is no extra CPU cycles spent. This trades off execution time for extra memory storage in the no-exception case and more CPU cycles in the exception case.

As I understand it, the Python "compiler" just generates a bunch of straight-line code and decision statements so that when a function returns, its result is checked and if the result is that an exception was thrown it jumps to the local handling code otherwise it jumps to the local non-exception code. The price for handling exceptions is always paid all the way up the stack for every function call made.

Because exceptions are a fundamental control-flow mechanism in Python (unlike in C++, where they should only be used for exceptional control flow), I'm not sure if there will be a net benefit to "zero cost" exceptions. I guess they should try it and measure the difference under various scenarios and make an informed decision based on evidence.

Re:Python 3.11: “Zero cost” exception handling by formerly_proven on Wednesday October 06, 2021 @10:36AM (#28773673)
Excellent post! Python Bytecode is a little more naive / high level than this though, so stuff like exceptions and their nested handlers are actually implemented in the VM itself. The VM has essentially a second stack for exception and context manager blocks. The compiled bytecode essentially looks like this:

    SETUP_TRY 10 # address where exceptions will be handled
    some stuff that might explode
    POP_BLOCK
    JUMP 20  # jump over exception handlers
    LOAD exception type  # address 10
    COMPARE  # check if exception type matches
    ... handler for the type
    stuff after the try-except  # address 20
The corresponding source would look like

    try:
        some stuff that might explode
    except exception type:
         ... handler for the type
    stuff after the try-except
Python's bytecode compiler is generally a 1:1 translation of the AST; it never optimizes, e.g:

    [value for value in list]
Translates to something like

    LIST_NEW
    FOR_EACH
    STORE
    LOAD
    LIST_APPEND
    JUMP BACK
Note how "value" generated a store-load pair.

The VM checks whether PyErr (pointer to exception) is set after basically everything. Similarly, extension modules check PyErr after every call into the interpreter, e.g.

    PyObject *attr = PyObject_GetAttr(someattr, somepystr);
    if(PyErr_Occurred()) { // or !attr
        // handle exception
    }
This gets old pretty fast.
Re:Python 3.11: “Zero cost” exception handling by mixedmath on Wednesday October 06, 2021 @12:12PM (#28774830)
I'm growing more interested in actually understanding some of the internals that you mention here. I know this is a bit of a tangent, but is there a better way to approach understanding python's internals than reading the source (which feels a bit monolithic to me right now)?
Re:Python 3.11: “Zero cost” exception handling by formerly_proven on Wednesday October 06, 2021 @12:23PM (#28774959)
The central dispatch of the VM is a good place to start: https://github.com/python/cpython/blob/main/Python/ceval.c#L...
Re:Python 3.11: “Zero cost” exception handling by boothby on Wednesday October 06, 2021 @12:26PM (#28774993)
I thought I had a pretty good handle on Python internals, until some time early this year when I took an interest in the generated bytecode. I'd read plenty of the cpython source, written lots of cython extensions, etc., but somehow missed the middle piece.

Fortunately, it's really easy to get at the bytecode, and quite instructive. Random inquiry: how do generator functions work?

  In [1]: def foo(): 
     ...:     yield from range(10) 
     ...:                                                                                                                                                                                                            

  In [2]: import dis                                                                                                                                                                                                 

  In [3]: dis.dis(foo)                                                                                                                                                                                               
    2           0 LOAD_GLOBAL              0 (range)
                2 LOAD_CONST               1 (10)
                4 CALL_FUNCTION            1
                6 GET_YIELD_FROM_ITER
                8 LOAD_CONST               0 (None)
               10 YIELD_FROM
               12 POP_TOP
               14 LOAD_CONST
               16 RETURN_VALUE
From there, you can read how each of those bytecode instructions is implemented in ceval.c, which formerly_proven links to.

edit: probably nice to have the actual disassembly of a list comprehension, too:

  In [4]: def bar(): 
     ...:     return [x for x in range(10)] 
     ...:                                                                                                                                                                                                            
  
  In [5]: dis.dis(bar)                                                                                                                                                                                               
    2           0 LOAD_CONST               1 (<code object <listcomp> at 0x7f814059f450, file "<ipython-input-4-a7a3cc7e7d7f>", line 2>)
                2 LOAD_CONST               2 ('bar.<locals>.<listcomp>')
                4 MAKE_FUNCTION            0
                6 LOAD_GLOBAL              0 (range)
                8 LOAD_CONST               3 (10)
               10 CALL_FUNCTION            1
               12 GET_ITER
               14 CALL_FUNCTION            1
               16 RETURN_VALUE
  
  Disassembly of <code object <listcomp> at 0x7f814059f450, file "<ipython-input-4-a7a3cc7e7d7f>", line 2>:
    2           0 BUILD_LIST               0
                2 LOAD_FAST                0 (.0)
          >>    4 FOR_ITER                 8 (to 14)
                6 STORE_FAST               1 (x)
                8 LOAD_FAST                1 (x)
               10 LIST_APPEND              2
               12 JUMP_ABSOLUTE            4
          >>   14 RETURN_VALUE
Re:Python 3.11: “Zero cost” exception handling by Chris2048 on Sunday October 10, 2021 @01:13PM (#28820073)
> Python's bytecode compiler is generally a 1:1 translation of the AST; it never optimizes

I thought python had a "peephole optimizer" that optimises?

Re:Python 3.11: “Zero cost” exception handling by zeotroph on Wednesday October 06, 2021 @10:28AM (#28773590)
Note that the generated instructions - even if they are never run as in [1] - still consume cache and might hinder further optimizations. Which is why `noexcept` is becoming more popular. And because there is no GC, code must be written to be exception-safe in all conditions which is often forgotten.

1: https://godbolt.org/z/bKfG14P64 - the difference between `e()` and `n()` is that one is marked `noexcept`. Both `f()` and `g()` run directly to `ret` (return) in the normal case.

Re:Python 3.11: “Zero cost” exception handling by bregma on Wednesday October 06, 2021 @11:03AM (#28773992)
Yeah, noexcept(true) is identical to wrapping the function with a try-catch construct in which the catch clause simply calls std::terminate(). Your godbolt example doesn't include the definition of n() so it doesn't show that. Adding noexcept(true) has a cost (because it's a kind of exception handling) but also allows the compiler to optimize out some of that cost under some circumstances.

Nevertheless, an example that demonstrates the exception path doesn't say much about the "zero-cost" non-exception path. In C++ exception handling is expensive. Outside of actually directly handling exceptions they have zero cost. There are no generated instructions. They do not consume cache. They do not hinder further optimizations.

Re:Python 3.11: “Zero cost” exception handling by zeotroph on Wednesday October 06, 2021 @12:23PM (#28774968)
Indeed, in the following case of a 4-deep call stack, each with its own exception, the bulk of the handling code can be moved elsewhere (already marked as cold by the compiler), but nonetheless there are a few instructions which won't matter in most cases but are still required to jump there and thus end up in the instruction cache.

https://godbolt.org/z/eh1d4K1M7

Re:Python 3.11: “Zero cost” exception handling by vlovich123 on Wednesday October 06, 2021 @11:03AM (#28773985)
Isn’t exception handling code placed in the cold section of the binary these days so that the impact on cache is nonexistent?

Yup, the generated assembly does this. There’s some minimal extra instructions still to setup the frame but the bulk of the exception handling lives elsewhere.

Re:Python 3.11: “Zero cost” exception handling by pjmlp on Thursday October 07, 2021 @02:12AM (#28782775)
There is GC in C++/CLI, Unreal C++, and the optional C++11 API, which is getting dropped in C++23, though.
Re:Python 3.11: “Zero cost” exception handling by Diggsey on Wednesday October 06, 2021 @11:47AM (#28774560)
Also, it's not really the case that exceptions (when not thrown) are zero-cost in C++, and not because of the instruction cache or increase to static data size.

The cost is that exception-throwing functions inhibit many of the optimizations performed by compilers, so they generate worse code, even though no "extra" code is actually executed.

Re:Python 3.11: “Zero cost” exception handling by bregma on Wednesday October 06, 2021 @02:40PM (#28776658)
Do they? I spend a lot of time dealing with the optimizers in GCC and haven't noticed that happening. In GCC, a throw is implemented as a single function call to the C++ runtime function __cxa_throw(). If a simple single function call will pessimize performance we're screwed as a species.
Re:Python 3.11: “Zero cost” exception handling by lgg on Wednesday October 06, 2021 @03:22PM (#28777146)
It does because calling that function adds a new control flow edge out of the block it is in, which means you can no longer prove certain cleanup code can safely be elided.

Additionally the fact that C++ does not annotate whether a function throws in its signature that means you need to generate unwind data for most non-leaf functions since (short of whole program LTO) there is no way to safely to know that the functions they call don't throw. That means the size of the metadata necessary to support unwinding grows with the size of your binary, not the portion of the code using exceptions.

Finally, C++ exceptions are dynamic, which imposes a higher cost when they are actually used than a static exception model.

In general I suspect that explicit error handling (as is done in the Swift ABI) would result in better code because it would make the control flow edges more optimizable and optimizer could safely assume many functions don't ever need to be unwound. It would certainly make the binaries smaller due to the reduction in size of unwind tables (although some people including Stroustrop[1] argue there are ways to do similar optimizations to elide unwind data with existing C++ compilers, but I am not sure if anyone has ever built such an optimizer).

There is some discussion about adding a static exception model (with annotated functions) described in P0709[2]. Various parts of that are controversial, though I hope the committee eventually finds a way to agree to some variant of it, because it would allow unification with other languages (it is semantically equivalent to Swift Error handling), and there are even C proposals that would be interoperable[3].

Full disclosure, I work on a dynamic linker written in C++, so I spend all day long writing C++ code that is built without exceptions or most of the standard library, but is part of the runtime machine that enables exception handling.

[1]: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p194...

[2]: http://open-std.org/JTC1/SC22/WG21/docs/papers/2019/p0709r4....

[3]: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2429.pdf

Re:Python 3.11: “Zero cost” exception handling by mplanchard on Wednesday October 06, 2021 @10:11AM (#28773406)
This was really informative, thanks! Also “it’s just like sufficiently advanced technology” made my morning.
Re:Python 3.11: “Zero cost” exception handling by coldtea on Wednesday October 06, 2021 @11:53AM (#28774632)
>Because exceptions are a fundamental control-flow mechanism in Python (unlike in C++, where they should only be used for exceptional control flow), I'm not sure if there will be a net benefit to "zero cost" exceptions.

Well, they might be "a fundamental control-flow mechanism" but for every StopIteration (for an example of a control flow exception use), there are multiple (up to millions) of traversed elements that didn't throw an exception.

Re:Python 3.11: “Zero cost” exception handling by Someone on Wednesday October 06, 2021 @10:31AM (#28773624)
One of the stated benefits of this is

> Calls to Python functions would be faster as frame objects would be considerably smaller. Currently each frame carries 240 bytes of overhead for exception handling.

I guess that’s where this will pay even in a language/ecosystem where exceptions aren’t exceptional.

Re:Python 3.11: “Zero cost” exception handling by bregma on Wednesday October 06, 2021 @11:08AM (#28774063)
It's a time-space tradeoff. A few bytes less overhead on the stack, a few CPU cycles more overhead when raising an exception. Is it a net win? Show me empirical test results.
Re:Python 3.11: “Zero cost” exception handling by omegalulw on Wednesday October 06, 2021 @02:17PM (#28776347)
Thanks for the informative post.

> then the stack is unwound by interpreting the function table contents to step through the various CPU states in reverse order

Why do you need the exception function table contents for this. Presumably, you only want to destroy stack objects so isn't the call stuck necessary and sufficient to unroll all the way back to the function with the catch handler?

Also what are your thoughts on returning errors via a status vs exceptions. I love the former - they force you to somehow handle each error (pass it up or deal with it) vs exceptions where there is no accountability for a method that causally caused the exception.

Re:Python 3.11: “Zero cost” exception handling by bregma on Wednesday October 06, 2021 @02:36PM (#28776599)
You need to restore some execution context before invoking the destructors. The value of the implicit "this" parameter, for example, has to be set correctly before the call to the destructor. Depending on the CPU and calling conventions, other registers may need their values contextually restored.

Then there's the stack crawling itself. Again, depending on the CPU and calling conventions, just finding where the return value is stored for a function can require some gymnastics (aarch64 I'm looking at you here).

As to returning and handling errors via status returns instead of exceptions: you're factoring the cost of error handling into the good path. It's the opposite of zero-cost. I've never understood the argument that every piece of code needs to be able to handle every error that every subroutine it calls could ever encounter. No one even does that, and the end result is that "handle error by return codes" is usually faster than using exceptions, because the error handling ends up being not done at all. At least, that's my experience over 40 years of maintaining other people's code.

As to the performance of languages that force error status returns to be dealt with vs. exception handling? Profile. Get some numbers. Examine the bias of the person generating the numbers.

Re:Python 3.11: “Zero cost” exception handling by sonthonax on Wednesday October 06, 2021 @01:29PM (#28775810)
What’s a DSO?
Re:Python 3.11: “Zero cost” exception handling by wahern on Wednesday October 06, 2021 @02:20PM (#28776382)
Dynamic shared object, or what's usually understood generically (in non-language specific terms) as a [binary] module or in GUI apps a plugin--something loaded and linked dynamically by the program, not by the linker at compile time or at program startup. In the Unix/C ecosystem "DSO" is a common term to describe binary modules, without reference to specific binary formats like ELF or mach-O.

Especially in the land of Unix where dynamic linking has become very sophisticated and automated, many of these terms are losing their distinctive meanings as the various ways to link and load converge on the same underlying mechanisms which you can mix-and-match independently. But normally the difference between a DSO and a shared library is that shared library symbols (functions, global variables) are by default visible globally (at least on ELF; mach-O story is more complex), making them implicitly visible to subsequently loaded code, whether a shared library or DSO. On the other hand, you usually want all DSO symbols to remain private to the DSO by default, requiring the application at runtime to explicitly request a pointer to specific symbols as different DSOs might have symbols of the same name (especially true for plugins), leaving the application logic to decide which to use and when. Also, especially pertinent to Lua, Perl, Python, Ruby, and similar languages, a DSO usually implicitly relies on the main program to have loaded and exported certain core APIs, rather than the DSO explicitly linking to a shared library to provide them. (This still often requires special compile-time flags when building the DSO, and possibly also special flags to the main binary.) That ensures the VM/interpreter/engine and all DSOs are always calling into the same core runtime implementation, and ensures the main binary controls which that is. DSOs do often dynamically link to other shared libraries, but because of default symbol visibility semantics this can cause problems, such as two DSOs or a DSO and the main program compiled against different releases of OpenSSL with incompatible ABIs. (If you can manage to load two different version of OpenSSL, you have the reverse issue of accidentally passing objects between the two implementations.) Mitigations and resolutions for that problem differ between ELF-based and mach-O-based systems. (AIX is an outlier, which like Windows uses a PE-derived binary format that is significantly different than ELF or mach-O. Many of the semantic distinctions and jargon that developed around ELF don't make much sense in the context of PE.)

Re:Python 3.11: “Zero cost” exception handling by mjw1007 on Wednesday October 06, 2021 @01:51PM (#28776052)
Dynamic Shared Object (in this context, probably a .so file).
Re:Python 3.11: “Zero cost” exception handling by sonthonax on Wednesday October 06, 2021 @03:46PM (#28777491)
Ah!
Re:Python 3.11: “Zero cost” exception handling by albertzeyer on Wednesday October 06, 2021 @11:25AM (#28774272)
> exceptions are a fundamental control-flow mechanism in Python

Where do you have this from?

In almost all Python code I have seen, exceptions are still for, as the name says, exceptions. So in the common cases, there would be no exceptions raised. I would assume that the exception handling code (under `except ...`) will be run only a tiny fraction of times compared to the other code, at least in most cases.

I would argue, if one abuses exceptions for any sort of control flow logic, this is bad design.

See also the list of builtin exceptions: https://docs.python.org/3/library/exceptions.html

From those, yes, there is StopIteration and StopAsyncIteration, which are used for control-flow, but the handling of those is anyway internal in CPython, and so the discussion about zero cost does not apply, as it would not change this (as far as I understand the current proposal).

Otherwise, all other exceptions are not used for control-flow.

Re:Python 3.11: “Zero cost” exception handling by BiteCode_dev on Wednesday October 06, 2021 @11:29AM (#28774327)
The for loop in python is a try/except catching StopIteration in a trench coat.

Also, EAFP, and the context manager protocol.

Re:Python 3.11: “Zero cost” exception handling by albertzeyer on Wednesday October 06, 2021 @02:55PM (#28776833)
But that is what I meant, this StopIteration would not be affected by the zero cost optimization. The zero cost optimization is about compiled Python bytecode and the VM interpreter. The StopIteration is handled internally by CPython (not via Python bytecode).

For example, consider this function:

    def foo():
        for i in range(3):
            print(i)
There is no try-except in this code, and neither in its bytecode. So this is not affected by the zero cost optimization from the linked issue.

The bytecode is this:

  2           0 LOAD_GLOBAL              0 (range)
              2 LOAD_CONST               1 (3)
              4 CALL_FUNCTION            1
              6 GET_ITER
        >>    8 FOR_ITER                12 (to 22)
             10 STORE_FAST               0 (i)

  3          12 LOAD_GLOBAL              1 (print)
             14 LOAD_FAST                0 (i)
             16 CALL_FUNCTION            1
             18 POP_TOP
             20 JUMP_ABSOLUTE            8
        >>   22 LOAD_CONST               0 (None)
             24 RETURN_VALUE
The iteration logic is inside the `FOR_ITER` op. And that is purely handled inside CPython.

Also, this is again what I meant: In user code, you rarely would use custom exceptions for control flow. Please show me any example where you have seen otherwise.

So, again, what I wrote: I'm very confident that for most user code, the code path under `except ...:` is only rarely executed. Please show me any big Python project where this would not be the case. I doubt that there is any.

Also, btw, on EAFP, Guido van Rossum disagrees: https://mail.python.org/pipermail/python-dev/2014-March/1331...

Re:Python 3.11: “Zero cost” exception handling by fouric on Wednesday October 06, 2021 @02:10PM (#28776273)
> The for loop in python is a try/except catching StopIteration in a trench coat.

That's...horrifying, both from a computational efficiency perspective (due to the overhead of exceptions) and from a "beauty" perspective (exceptions are supposed to be exceptional, darn it!). Why is it implemented this way? Why not just use a normal loop and check a return value from the iterator?

Re:Python 3.11: “Zero cost” exception handling by Scramblejams on Wednesday October 06, 2021 @02:30PM (#28776512)
exceptions are supposed to be exceptional, darn it

This feeling is at odds with what is generally considered idiomatic Python, for better or worse.

Upthread BiteCode mentioned EAFP. See, for example: https://devblogs.microsoft.com/python/idiomatic-python-eafp-...

(It's certainly not unanimous. Guido himself is on the record as saying EAFP isn't better than LBYL, but in most of the Python I've cruised through these last umpteen years [that wasn't some other language written in Python], I've certainly seen an overall preference for EAFP. Naturally YMMV.)

Re:Python 3.11: “Zero cost” exception handling by fouric on Wednesday October 06, 2021 @05:45PM (#28778897)
I think that implies that either "exceptions" are misnamed in Python (as in every other language with that name, they're meant to be exactly that - exceptional - so whatever Python calls "exceptions" isn't the same as in other languages), or that that particular idiom is wrong, and needs to be rethought.
Re:Python 3.11: “Zero cost” exception handling by dragonwriter on Wednesday October 06, 2021 @02:14PM (#28776315)
> Why not just use a normal loop and check a return value from the iterator?

Because an iterator needs to be able to return any python value, and if you did that, there would be at least one python value that an iterator could not actually return.

Unless you required the iterator to wrap real return values in a container (basically, an optional/maybe monad) but then you have unwrapping overhead on every real return value, which is worse.

Re:Python 3.11: “Zero cost” exception handling by fouric on Wednesday October 06, 2021 @05:49PM (#28778939)
Couldn't the runtime just allocate a specific Python object representing the case where the iterator is complete? Make that object implementation-specific, hide it as best as you can, write documentation stating that users should not use this value directly, and if they do - they voided the warranty, and should expect evil to befall them. (ultimately, you can't prevent the user from doing really dumb things e.g. editing the Python binary directly)
Re:Python 3.11: “Zero cost” exception handling by dragonwriter on Wednesday October 06, 2021 @06:00PM (#28779042)
You do that with one thing, okay, maybe it works.

You do that with two or more of the protocols that Python has that use return values for return values ans exceptions for flow control signals and...you make it a lot harder to work with them, especially in conjunction.

Re:Python 3.11: “Zero cost” exception handling by fouric on Thursday October 07, 2021 @10:41AM (#28786902)
Can you give an example of this? I can't think of any myself - generators also raise a StopIteration exception, for instance, so they could use the same value.
Re:Python 3.11: “Zero cost” exception handling by carapace on Wednesday October 06, 2021 @12:34PM (#28775096)
In Python e.g. instead of doing this:

    if key in some_dict:
        foo = some_dict[key]
        ... # A
    else:
        ... # B
you would do this:

    try:
        foo = some_dict[key]
    except KeyError:
        ... # B
    else:
        ... # A
Re:Python 3.11: “Zero cost” exception handling by albertzeyer on Wednesday October 06, 2021 @02:57PM (#28776863)
You can do that, but:

- Even in this example, I would assume that branch A is more often executed. So the zero cost optimization (which is about branch A) would improve this code performance.

- I would argue, code which uses `some_dict.get(key, fallback)` or explicitly check `if key in some_dict: ...` is both more clean and more Pythonic. See also e.g.: https://mail.python.org/pipermail/python-dev/2014-March/1331...