Live data from Hacker News

Python 3.11: “Zero cost” exception handling

bugs.python.org

81–90 of 96 posts

Re: Python 3.11: “Zero cost” exception handling

#81

Earlier quoted context omitted.

> 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…

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

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...

Re: Python 3.11: “Zero cost” exception handling

#82
post #79
post #53

Earlier quoted context omitted.

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.

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.

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

#83
post #64

Wow! Prior to reading this, I was not aware of "Zero Cost" exception handling. While I am only a Python developer, I always assumed that in any programming language, exception handling, regardless of whether an exception is raised or not, cost some CPU cycles. I work at an HFT firm and they test their changes in equations in Python programs on crypto rather than C++. So I resorted to using try-except blocks in Python…

For me it isn't about the cost. Modern languages like Go and Rust separate the error handling from the conventional logic, and that makes the code more readable. It's my only complain about Python, (outside of performance of course). In Python when you see a `try`, you don't know if it's because there's error handling going on, or if it's because that's the only way to achieve a certain goal due to Python being desig…

> Modern languages like Go and Rust separate the error handling from the conventional logic, and that makes the code more readable.

The (result, error) pattern in Go or Result pattern in Rust usually mixes the two. Unless you're doing and_then in Rust, but Go doesn't have anything like that. If anything, I feel like it's exceptions that separate the error handling for the conventional logic. You have your normal code in try, and your error handling in except.

Re: Python 3.11: “Zero cost” exception handling

#84
post #21

The tenacity of people getting excited over micro optimizations in Python for more than two decades is remarkable. Nothing has happened despite monumental speed programs that were broadly advertised and marketed to corporations. Meanwhile, SBCL has an industrial strength compiler that predates Python and its trademark (the SBCL compiler was called "Python" before the trademark, thereby invalidating it). Python (the l…

I think the worst part is them not optimizing CPython earlier, which means that people now rely a lot on CPython internals, which means stuff like Pypy isn't compatible with the Python ecosystem. And then you end up with everyone having their Python optimization: Dropbox, Instagram, Pypy.

Re: Python 3.11: “Zero cost” exception handling

#85
post #21

The tenacity of people getting excited over micro optimizations in Python for more than two decades is remarkable. Nothing has happened despite monumental speed programs that were broadly advertised and marketed to corporations. Meanwhile, SBCL has an industrial strength compiler that predates Python and its trademark (the SBCL compiler was called "Python" before the trademark, thereby invalidating it). Python (the l…

It's when you put all these small optimizations together that it leads to something remarkable. It's analogous to video codecs: there are a bunch of individual optimizations that alone don't look that impressive, only saving ~1% here or there. But once they all are working together, you see savings of 10-50%.

I think what's surprising is how small these optimizations are. A few proposal sped up Python a lot, but were partially incompatible. Meanwhile OCaml is removing the GIL from the language, adding multicore and added stuff like Flambda in the last decade, all of that while mostly keeping backwards compatibility and not sacrificing single-core performance. All of that while having way less people working on it than Python.

Re: Python 3.11: “Zero cost” exception handling

#87
post #64

Wow! Prior to reading this, I was not aware of "Zero Cost" exception handling. While I am only a Python developer, I always assumed that in any programming language, exception handling, regardless of whether an exception is raised or not, cost some CPU cycles. I work at an HFT firm and they test their changes in equations in Python programs on crypto rather than C++. So I resorted to using try-except blocks in Python…

For me it isn't about the cost. Modern languages like Go and Rust separate the error handling from the conventional logic, and that makes the code more readable. It's my only complain about Python, (outside of performance of course). In Python when you see a `try`, you don't know if it's because there's error handling going on, or if it's because that's the only way to achieve a certain goal due to Python being desig…

> Modern languages like Go and Rust separate the error handling from the conventional logic, and that makes the code more readable.

This is a very odd claim. Go and Rust are extreme examples of mixing up error handling with conventional logic. There are excellent reasons for doing it that way, but the fact remains that they do. Exceptions, on the other hand, definitely do separate error handling from conventional logic – that's the whole point of them.

I think you just happen to have seen Python code bases where exceptions are caught very close to where they are being thrown, but that's property of the code you read, not the language feature. And presumably you have also seen Rust/Go code bases where errors are often passed back up the stack, which is easy to do but still requires some code (even just a ? in Rust is still an explicit decision) in a way that allowing exceptions to propagate up does not.

Re: Python 3.11: “Zero cost” exception handling

#88
post #71

Earlier quoted context omitted.

> 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?

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 la…

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

#89
post #71

Earlier quoted context omitted.

> 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?

> 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 v…

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

#90
post #89

Earlier quoted context omitted.

> 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 v…

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. edit…

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.

Post reply on HN