cpython icon indicating copy to clipboard operation
cpython copied to clipboard

Avoid `exc=None; del exc` in bytecode where unnecessary

Open sweeneyde opened this issue 3 years ago • 3 comments

See https://github.com/faster-cpython/ideas/issues/490.

To break reference cycles, the compiler has an implicit del e at the end of an exception handler. To make UnboundLocalErrors impossible during that deletion, the compiler adds e=None; del e instead.

However, the e=None part is unnecessary in many situations, namely, when we can prove that e is guaranteed to be bound (almost always!).

This can reduce the size of some bytecode objects. For example, the four <---- lines below can be deleted:

>>> def f():
...     try: pass
...     except Exception as e: pass
...
>>> dis(f)
  1           0 RESUME                   0

  2           2 LOAD_CONST               0 (None)
              4 RETURN_VALUE
              6 PUSH_EXC_INFO

  3           8 LOAD_GLOBAL              0 (Exception)
             20 CHECK_EXC_MATCH
             22 POP_JUMP_IF_FALSE       11 (to 46)
             24 STORE_FAST               0 (e)
             26 POP_EXCEPT
             28 LOAD_CONST               0 (None)  <---------
             30 STORE_FAST               0 (e)     <---------
             32 DELETE_FAST              0 (e)     
             34 LOAD_CONST               0 (None)
             36 RETURN_VALUE
             38 LOAD_CONST               0 (None)  <---------
             40 STORE_FAST               0 (e)     <---------
             42 DELETE_FAST              0 (e)
             44 RERAISE                  1
        >>   46 RERAISE                  0
        >>   48 COPY                     3
             50 POP_EXCEPT
             52 RERAISE                  1
ExceptionTable:
  6 to 24 -> 48 [1] lasti
  38 to 46 -> 48 [1] lasti
  • PR: gh-99361

sweeneyde avatar Nov 11 '22 04:11 sweeneyde

I believe e is always guaranteed to be bound, by construction. My assumption is that the None is assigned to break the cycle so that gc would have fewer cycles to deal with (exception tracebacks reference the frame, and this is a reference from the frame to the exception).

I guess benchmarking would tell.

iritkatriel avatar Nov 11 '22 05:11 iritkatriel

I believe e is always guaranteed to be bound, by construction.

Not in the presence of an extra manually-added del e, right?

def f():
    try:
        1/0
    except ZeroDivisionError as e:
        del e
        print(e)

        
f()
Traceback (most recent call last):
  File "<pyshell#13>", line 3, in f
    1/0
ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<pyshell#14>", line 1, in <module>
    f()
  File "<pyshell#13>", line 6, in f
    print(e)
UnboundLocalError: cannot access local variable 'e' where it is not associated with a value

sweeneyde avatar Nov 11 '22 05:11 sweeneyde

It looks like the e = None; del e code was first added in issue https://github.com/python/cpython/issues/44437, commit https://github.com/python/cpython/commit/b940e113bf90ff71b0ef57414ea2beea9d2a4bc0.

The mailing list thread leading up to the issue is here: https://mail.python.org/pipermail/python-3000/2007-January/005384.html

  • Folks are discussing avoiding reference cycles, potentially by using weakrefs
  • Phillip J. Eby proposes always using del e, Guido is +1
  • Phillip J. Eby: """Actually, on second thought it occurs to me that the above code isn't a 100% correct translation, because if "body" contains its own del e, then it will fail."""
  • Ka-Ping Yee proposes e = None
  • Phillip J. Eby proposes e = None; del e, that what gets implemented.
  • The benefits outweigh this introduction of something like a "block scope"

sweeneyde avatar Nov 11 '22 05:11 sweeneyde