[HN Gopher] Python 3.11: "Zero cost" exception handling
       ___________________________________________________________________
        
       Python 3.11: "Zero cost" exception handling
        
       Author : rbanffy
       Score  : 181 points
       Date   : 2021-10-06 11:48 UTC (11 hours ago)
        
 (HTM) web link (bugs.python.org)
 (TXT) w3m dump (bugs.python.org)
        
       | ioejoijweoij wrote:
       | Go developers are so screwed. They tried very hard to find some
       | excuse for writing 4 times more lines of code than other
       | languages. and jet, exception handling has zero additional costs.
       | Their last argument is debunked. Go is one of the worst languages
       | ever made.
        
       | cnband wrote:
       | 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
       | language) is mainly good at aggressive marketing.
        
         | Zababa wrote:
         | 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.
        
         | ok123456 wrote:
         | 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%.
        
           | kzrdude wrote:
           | I'm looking forward o seeing numbers per python release, for
           | what the optimizations result in taken together. Mark Shannon
           | is going at it with optimizations now.
        
           | Zababa wrote:
           | 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.
        
       | pythonb3sss wrote:
       | 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 to reduce
       | "branching" i.e if-elif-else blocks. I would just add all the
       | different conditional functions in a dictionary and manage calls
       | based on keys and handle exceptions. I don't know if that's the
       | best way to improve speed, but I would like to check if this has
       | any impact on it.
        
         | olah_1 wrote:
         | You may enjoy programming in Elixir if you like that style. In
         | Elixir, you _only_ program the "happy path" and just let things
         | fail. Then you rely on supervisor processes to handle the
         | exceptions /errors. Well, at least that is the idea. I think
         | people still do tests and function guards and things. but the
         | "let it fail" idea is definitely part of the Erlang/Elixir
         | world.
         | 
         | The sad thing is that there really isn't any "learn elixir"
         | book that teaches this idiomatic design. A student of Elixir
         | should set up an umbrella application from the very first hello
         | world, in my opinion.
        
           | pythonb3sss wrote:
           | Well, I don't _enjoy_ the  "happy path" programming.
           | Admittedly, this implementation to improve speed feels a bit
           | hacky. I only did it because it had a measurable impact on
           | the computational performance of my program. In my other
           | grunt worker scripts, I actually prefer if-elif-else
           | statements because they make code readability better for
           | other programmers who are not Python "natives", but use the
           | scripts or modify them to suit their use cases.
        
         | Lendal wrote:
         | 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 designed to
         | mingle logic with error handling. After doing projects in Go
         | and Rust, I can see the value in separating the two, and that
         | makes me sad that Python is old now.
         | 
         | Maybe what they're planning to do with this is allow wrappers
         | to hide the places where exception handling is gratuitous, and
         | therefore try to bring Python forward into the world of more
         | modern languages.
        
           | Zababa wrote:
           | > 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<Ok(res),
           | Err(error)> 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.
        
           | quietbritishjim wrote:
           | > 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.
        
         | klodolph wrote:
         | 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 was faster. It sounds
         | like the fast path in the second case is getting faster, so
         | this performance gap may be increasing.
        
           | pythonb3sss wrote:
           | I am going to speculate here, so if I'm wrong please point it
           | out.
           | 
           | Here, the number of steps directly affect the time.
           | 
           | In the first approach, the ".get()" method first analyses the
           | type of "some_dict" and then uses an internal variable (the
           | ones surrounded by double underscores) to try and fetch the
           | value by using the provided key. If the key is present, then
           | the value is returned, if not then a default value is
           | returned. So if the key does not exist, the returning the
           | default value saves 1 step (that of fetching the value from
           | the map)
           | 
           | In the second approach, the exception raises the number of
           | steps because the type of error has to be determined and the
           | stack is traced every time an exception is raised. So the
           | more exceptions are raised, the slower the code gets.
           | 
           | I tested this with 3.9.7 right now and in my testing, the
           | runtime of first approach was virtually unchanged, while the
           | second one was faster if exceptions were raised ~12% of the
           | time or less. (I ran both 10 million times)
        
           | ynik wrote:
           | 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.
        
             | klodolph wrote:
             | > If the performance of hash lookups matters, Python is the
             | wrong language for you.
             | 
             | Announcement to Python programmers: "Don't bother trying to
             | improve the performance of your Python code! If performance
             | matters, just completely rewrite your code in a different
             | language!"
             | 
             | I don't know how to respond to that, except to disagree
             | with the underlying assumptions that (1) there is a "right
             | language", (2) if performance matters, Python is _not_ a
             | suitable language, or (3) people are generally in a
             | position to choose which language a project is written in.
             | 
             | Even if performance matters, it is not the only thing that
             | matters. When you choose a language, there are necessarily
             | tradeoffs... everything from the skillset of your team, to
             | the ecosystem of libraries available affects that decision.
             | Finally, there are projects already written in Python.
        
       | ziml77 wrote:
       | 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.
        
         | masklinn wrote:
         | > In a language where idiomatic control flow uses exceptions?
         | That's crazy!
         | 
         | Seems like the opposite: if exceptions are extremely rare then
         | you want to optimise the case where they're not raised, at the
         | cost of the other one.
         | 
         | If exceptions are common then it matters a lot less, you may
         | even want to avoid 0ce depending on the impact on the raised
         | case.
        
           | alex_smart wrote:
           | I don't think python has the mindset of "exceptions are
           | extremely rare". That is probably what the OC meant by python
           | being "a language where idiomatic control flow uses
           | exceptions". As an example, every iterator in python signals
           | its end by throwing a StopIteration exception. So, every "for
           | x in iter" has the interpreter throwing and catching an
           | exception.
        
             | masklinn wrote:
             | > I don't think python has the mindset of "exceptions are
             | extremely rare".
             | 
             | No it does not, that's my point: when exceptions are
             | extremely rare, as in C++ or Java, having the "no
             | exception" case be free at additional expense in the
             | "exception" case is an excellent tradeoff, because the
             | latter should happen extremely rarely so even if the costs
             | are ridiculous you'll have more than made them by all the
             | cases where you got the "no exception" run for free.
             | 
             | In Python where exceptions are common however, making one
             | case free at the expense of the other is less interesting a
             | proposition, and is more of a balancing act: you don't want
             | to overly penalise exceptions-heavy code as that is
             | considered normal and idiomatic.
        
         | bregma wrote:
         | 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.
        
           | mplanchard wrote:
           | This was really informative, thanks! Also "it's just like
           | sufficiently advanced technology" made my morning.
        
           | formerly_proven wrote:
           | 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.
        
             | mixedmath wrote:
             | 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)?
        
               | formerly_proven wrote:
               | The central dispatch of the VM is a good place to start: 
               | https://github.com/python/cpython/blob/main/Python/ceval.
               | c#L...
        
               | boothby wrote:
               | 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
        
           | albertzeyer wrote:
           | > 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.
        
             | BiteCode_dev wrote:
             | The for loop in python is a try/except catching
             | StopIteration in a trench coat.
             | 
             | Also, EAFP, and the context manager protocol.
        
               | fouric wrote:
               | > 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?
        
               | Scramblejams wrote:
               | _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.)
        
               | fouric wrote:
               | 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.
        
               | dragonwriter wrote:
               | > 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.
        
               | fouric wrote:
               | 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)
        
               | dragonwriter wrote:
               | 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.
        
               | albertzeyer wrote:
               | 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...
        
             | carapace wrote:
             | 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
        
               | albertzeyer wrote:
               | 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...
        
           | sonthonax wrote:
           | What's a DSO?
        
             | mjw1007 wrote:
             | Dynamic Shared Object (in this context, probably a .so
             | file).
        
               | sonthonax wrote:
               | Ah!
        
             | wahern wrote:
             | 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.)
        
           | omegalulw wrote:
           | 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.
        
             | bregma wrote:
             | 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.
        
           | Diggsey wrote:
           | 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.
        
             | bregma wrote:
             | 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.
        
               | lgg wrote:
               | 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
        
           | zeotroph wrote:
           | 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.
        
             | vlovich123 wrote:
             | 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.
        
             | bregma wrote:
             | 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.
        
               | zeotroph wrote:
               | 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
        
           | coldtea wrote:
           | > _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.
        
           | Someone wrote:
           | 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.
        
             | bregma wrote:
             | 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.
        
         | jerf wrote:
         | This is on a different dimension.
         | 
         | Up to this point, in Python, exceptions were always checked, so
         | they were "free" in the sense you were always paying for them
         | anyhow. I remember many C++ programmers migrating to Python
         | back in the ol' comp.lang.python days having this described to
         | them. You don't have to worry about exception-heavy code,
         | because since you're always paying for them anyhow, you might
         | as well use them.
         | 
         | A lot of making CPython code run well works like that. You want
         | to use as much of the stuff you're already always paying for
         | anyhow, rather than reimplementing any of it in your own code.
        
         | pca006132 wrote:
         | Zero cost refers to the cost when _no exception is thrown_ ,
         | not the overhead of exceptions. It may be more expensive
         | throwing an exception under "zero cost" exception model, as
         | throwing an exception may require parsing some data in the
         | executable. (I'm not sure about the implementation, so this is
         | just a may...)
        
           | SideburnsOfDoom wrote:
           | > Zero cost refers to the cost when no exception is thrown,
           | not the overhead of exceptions
           | 
           | There are recent benchmarks where .NET "zero cost when an
           | try-catch block is present and exception not thrown" turned
           | out to be significantly slower then the alternative without
           | such a block.
           | 
           | It turns out that the try-catch block is a barrier across
           | which some optimisations and re-organisations (e.g. method
           | inlining) cannot happen, and that the compiler & JIT normally
           | do a lot of these. So it might be "zero cost" but it might
           | also prevent wins.
           | 
           | Edit: see here https://blogs.msmvps.com/peterritchie/2007/06/
           | 22/performance...
           | 
           | https://stackoverflow.com/questions/1308432/do-try-catch-
           | blo...
        
             | masklinn wrote:
             | Makes sense. Basically it's zero-cost for exceptions-
             | unaware code. I don't know if it's been changed with more
             | modern jits but used to be chrome was unable to jit
             | functions with try statements (similar to functions using
             | `arguments` or `with` I think).
        
         | [deleted]
        
         | zohch wrote:
         | > I've felt weird using exceptions like that
         | 
         | How should they be used instead? Maybe I don't understand what
         | you mean by "idiomatic control flow uses exceptions" - could
         | you give an example. Maybe there is some use of exceptions that
         | I'm not quite familiar with in Python.
        
           | ubercore wrote:
           | The first that came to mind is how `get()` is handled in
           | Django's ORM. The idiomatic way to look for a single object
           | is to use `get`, then catch a `DoesNotExist` exception:
           | 
           | From https://docs.djangoproject.com/en/3.2/ref/models/queryse
           | ts/#...                 from django.core.exceptions import
           | ObjectDoesNotExist            try:           blog =
           | Blog.objects.get(id=1)           entry =
           | Entry.objects.get(blog=blog, entry_number=1)       except
           | ObjectDoesNotExist:           print("Either the blog or entry
           | doesn't exist.")
        
             | brianwawok wrote:
             | Right, but the better way to actually write this is
             | something like                 entry =
             | Entry.objects.filter(blog__id=1,
             | entry_number=1).first()       if entry is None:         #
             | deal with does not exist
             | 
             | Maybe it's my scala/Java background shining through, but we
             | are big Django users and we ban the "catch exceptions as
             | standard" workflow, because there is almost always a
             | cleaner way...
        
               | dec0dedab0de wrote:
               | I wouldn't make it an if statement unless it's going to
               | be a part of the standard flow. I think the catchphrase
               | is "leap before you look". Though your right that a
               | single query is better.
               | 
               | Honestly, I normally just use get and let the exception
               | fly. If it's a celery task I'll see the stack trace in
               | flower, or right in the output if it's dev with debug on.
               | Then I would go out of my way to make sure there was
               | never a circumstance where a user requests something that
               | doesn't exist.
        
               | _bohm wrote:
               | This fails to raise an error if there is more than one
               | object matching the given filters
        
               | notdonspaulding wrote:
               | Presumably entry_number is unique_together with blog_id.
               | Otherwise the original code is also not handling the
               | MultipleObjectsReturned exception.
               | 
               | Generally speaking, I tend toward the cleanest code
               | being:                   blog = Blog.objects.get(id=1)
               | entry = blog.entry_set.filter(entry_number=1).first()
               | if entry is None:             handle_missing_entry()
               | handle_entry(entry)
               | 
               | But it does suffer from having the extra DB query in
               | there, which may or may not be helpful, depending on the
               | surrounding code (and whether or not you'll be using the
               | blog instance anywhere else).
        
               | jrochkind1 wrote:
               | What are you suggesting is better about that way? More
               | readable?
        
               | zohch wrote:
               | > Right, but the better way to actually write this is
               | something like
               | 
               | Maybe for some cases, but it does not do the exact same
               | thing, see the comment here:
               | https://stackoverflow.com/a/29455777/1598080
               | 
               | And if we are talking about idiomatic, I think it is
               | maybe a stretch to count this as idiomatic for Python,
               | but given it is documented for Django I think it is fair
               | to call it idiomatic for Django.
        
               | ziml77 wrote:
               | That is the sort of method that I prefer if it's
               | available. I'm a C# dev so this way also feels far
               | cleaner to me.
        
             | louissan wrote:
             | Or even?:
             | 
             | try: blog = Blog.objects.get(slug__icontains='some text')
             | except Blog.ObjectDoesNotExist: print("No blog could be
             | found") except Blog.MultipleObjectsReturned: print('More
             | than one blog!')
        
           | ehvatum wrote:
           | https://www.educba.com/python-stopiteration/
        
           | foobarbecue wrote:
           | In python, it's normal to use exceptions in place of type
           | checking, e.g. in polymorphic functions.
        
             | zohch wrote:
             | But that would not be for control flow.
        
               | foobarbecue wrote:
               | On a smaller scale, it is.
        
               | zohch wrote:
               | To me sending wrong arguments is an error condition, not
               | control flow, see this for more info: https://softwareeng
               | ineering.stackexchange.com/questions/1892...
        
               | ziml77 wrote:
               | It's not about sending the wrong arguments. In Python a
               | function may support a variety of types in a single
               | argument. But instead of querying the type and switching
               | based on that, you often want to try to call a method and
               | if you get an exception (because that method does not
               | exist), try to call a different method that gets you what
               | you need.
               | 
               | The advantage to this over checking the type is that you
               | are still use duck typing. If you check the type then you
               | can only support a specific set of classes.
        
               | gpderetta wrote:
               | This is also known in Python as the "ask for forgiveness
               | not permission" idiom.
        
               | zohch wrote:
               | Thanks for the clarification, I have actually seen this
               | now that I think of it and this indeed is closer to
               | control flow than error flow.
        
           | [deleted]
        
           | openasocket wrote:
           | When using a for-loop over an iterator, the iterator protocol
           | in Python says to keep returning elements until you run out,
           | at which point you throw an exception. So every loop over an
           | iterator or iterable object in python throws an exception
           | when it is done. https://docs.python.org/3/library/stdtypes.h
           | tml#iterator.__n...
        
             | ynik wrote:
             | While that's true at the Python language level, there are
             | already special optimizations for this in CPython:
             | `tp_iternext` is not required to set an exception. If it
             | returns NULL without setting an exception, that's taken to
             | be the end of iteration.
             | 
             | If you call `next()` in Python, this special case is
             | translated to a `StopIteration` exception. But if you use a
             | for-loop, it can directly stop iterating without ever
             | materializing the `StopIteration` exception. So the
             | overhead of Python raise/try-except is already irrelevant
             | for the for-loop.
        
             | zohch wrote:
             | TIL, thanks for explaining, indeed would have expected
             | exceptions to be pretty cheap also if I knew this.
        
           | brandmeyer wrote:
           | Nearly all loops are terminated by raising an exception.
           | 
           | https://docs.python.org/3/library/exceptions.html#StopIterat.
           | ..
        
           | ghshephard wrote:
           | 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 in the exception you catch, and (B) keep it in
           | as small a portion of code as possible.
           | 
           | I just think it makes for clumsy code - which of the two look
           | better:                   try:             value =
           | dict_["key"]         except KeyError:             pass
           | else:             do_something(value)
           | 
           | OR                   if "key" in dict_:
           | do_something(dict["key"])
           | 
           | But it might just be me.
        
             | mywittyname wrote:
             | If you don't care about a key existing, then this works.
             | do_something(dict_.get("key", None))
             | 
             | I use that a lot for data parsing. Passing the exception is
             | not very clean IMHO. I stick to d[key] nomenclature when I
             | need assurance that all the keys are present in the
             | dictionary, and .get(key,None) when I don't.
        
               | stevesimmons wrote:
               | .get("key") is enough, as the default is already None.
               | 
               | And if you care about the default value being a
               | particular type, when there may also be None in the input
               | stream, do something like:
               | 
               | x.get("key") or []
               | 
               | or
               | 
               | str(x.get("key") or "") # Guarantee strings and avoid
               | "None"!
        
             | stevesimmons wrote:
             | 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.
        
               | jamincan wrote:
               | Doesn't Python pretty much guarantee that since it's
               | single-threaded?
        
               | [deleted]
        
               | detaro wrote:
               | 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.
        
             | zohch wrote:
             | > In particular, this is not idiomatic python
             | 
             | According to this article, I think his case is rather weak.
             | The conclusion does not seem to follow from the premise to
             | me.
        
             | dec0dedab0de wrote:
             | If I find if's and try's looking ugly for a particular use
             | case I try to figure out how to get rid of them. For your
             | first example I would do this, assuming value is a number.
             | value += dict_.get('key',0)
             | 
             | Though I agree with using an if in the second example, if
             | there isn't a better way to do the iteration to avoid
             | looking up keys that don't exist.
        
       ___________________________________________________________________
       (page generated 2021-10-06 23:02 UTC)