Live data from Hacker News

Python 3.11: “Zero cost” exception handling

bugs.python.org

51–60 of 96 posts

Re: Python 3.11: “Zero cost” exception handling

#51

Earlier quoted context omitted.

This covers it very well: https://devblogs.microsoft.com/python/idiomatic-python-eafp-... In particular, this is not idiomatic python: if "key" in dict_: value += dict_["key"] But this is: try: value += dict_["key"] except KeyError: pass I too hate using the exception handling in this way, and if you aren't careful, you end up papering over other unexpected exceptions in your code, so you have to be (A) very specific…

The difference in your "which of the two look better" example is: (a) has one dict operation plus an exception which rarely occurs and is nearly zero cost if it doesn't. versus (b) has nearly always two dict operations, plus a possibly incorrect assumption that the dict will not be mutated between the "if key in dict" and "dict[key]" operations.

Doesn't Python pretty much guarantee that since it's single-threaded?

Re: Python 3.11: “Zero cost” exception handling

#52

Earlier quoted context omitted.

The difference in your "which of the two look better" example is: (a) has one dict operation plus an exception which rarely occurs and is nearly zero cost if it doesn't. versus (b) has nearly always two dict operations, plus a possibly incorrect assumption that the dict will not be mutated between the "if key in dict" and "dict[key]" operations.

Doesn't Python pretty much guarantee that since it's single-threaded?

[deleted]

Re: Python 3.11: “Zero cost” exception handling

#53
post #22
post #2

They weren't zero cost before? In a language where idiomatic control flow uses exceptions? That's crazy! I've felt weird using exceptions like that but I always assumed that CPython was optimized to minimize overhead of exceptions and exception handlers.

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

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

#54
post #22
post #2

They weren't zero cost before? In a language where idiomatic control flow uses exceptions? That's crazy! I've felt weird using exceptions like that but I always assumed that CPython was optimized to minimize overhead of exceptions and exception handlers.

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

>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

#55
post #22

Earlier quoted context omitted.

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

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

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

#56

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…

There are some weird performance optimizations in Python, e.g., item = some_dict.get(key) if item is None: # key does not exist Versus try: item = some_dict[key] except KeyError: # key does not exist When I tested these (admittedly, a while ago), which one was faster depended on how often the key was missing. If “missing key” was an expected case, the first one was faster. If “missing key” was uncommon, the second wa…

Fun fact: all those approaches use multiple dict lookups, just of different dicts.

First approach is looking for `get` in `type(some_dict).__dict__` and then for `key` in `some_dict`. Second approach is looking for `key` in `some_dict`, and then (only if missing) for `KeyError` in the module globals/builtins.

If the performance of hash lookups matters, Python is the wrong language for you.

Re: Python 3.11: “Zero cost” exception handling

#57

Earlier quoted context omitted.

The difference in your "which of the two look better" example is: (a) has one dict operation plus an exception which rarely occurs and is nearly zero cost if it doesn't. versus (b) has nearly always two dict operations, plus a possibly incorrect assumption that the dict will not be mutated between the "if key in dict" and "dict[key]" operations.

Doesn't Python pretty much guarantee that since it's single-threaded?

No, you can have multiple threads in a python app. And while it promises to keep many things atomic, code like that can be interrupted between each line at minium.

Re: Python 3.11: “Zero cost” exception handling

#58

Earlier quoted context omitted.

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

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

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

#59
post #40

Earlier quoted context omitted.

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

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

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

#60

Earlier quoted context omitted.

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

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

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 ( at 0x7f814059f450, file "", line 2>)
                2 LOAD_CONST               2 ('bar..')
                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  at 0x7f814059f450, file "", 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
Post reply on HN