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.
Python 3.11: “Zero cost” exception handling
51–60 of 96 posts
Re: Python 3.11: “Zero cost” exception handling
#52Earlier 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?
Re: Python 3.11: “Zero cost” exception handling
#53They 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…
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
#54They 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…
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
#55Earlier 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…
Re: Python 3.11: “Zero cost” exception handling
#56Wow! 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…
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
#57Earlier 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?
Re: Python 3.11: “Zero cost” exception handling
#58Earlier 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)?
Re: Python 3.11: “Zero cost” exception handling
#59Earlier 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…
Re: Python 3.11: “Zero cost” exception handling
#60Earlier 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)?
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