Skip to content

Commit

Permalink
pythongh-93143: Avoid NULL check in LOAD_FAST based on analysis in th…
Browse files Browse the repository at this point in the history
…e compiler (pythonGH-93144)
  • Loading branch information
sweeneyde committed May 31, 2022
1 parent 8a5e3c2 commit f425f3b
Show file tree
Hide file tree
Showing 11 changed files with 371 additions and 52 deletions.
19 changes: 10 additions & 9 deletions Include/internal/pycore_opcode.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 16 additions & 15 deletions Include/opcode.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Lib/importlib/_bootstrap_external.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,7 @@ def _write_atomic(path, data, mode=0o666):

# Python 3.12a1 3500 (Remove PRECALL opcode)
# Python 3.12a1 3501 (YIELD_VALUE oparg == stack_depth)
# Python 3.12a1 3502 (LOAD_FAST_CHECK, no NULL-check in LOAD_FAST)

# Python 3.13 will start with 3550

Expand All @@ -419,7 +420,7 @@ def _write_atomic(path, data, mode=0o666):
# Whenever MAGIC_NUMBER is changed, the ranges in the magic_values array
# in PC/launcher.c must also be updated.

MAGIC_NUMBER = (3501).to_bytes(2, 'little') + b'\r\n'
MAGIC_NUMBER = (3502).to_bytes(2, 'little') + b'\r\n'

_RAW_MAGIC_NUMBER = int.from_bytes(MAGIC_NUMBER, 'little') # For import.c

Expand Down
4 changes: 3 additions & 1 deletion Lib/opcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,14 @@ def jabs_op(name, op):
def_op('COPY', 120)
def_op('BINARY_OP', 122)
jrel_op('SEND', 123) # Number of bytes to skip
def_op('LOAD_FAST', 124) # Local variable number
def_op('LOAD_FAST', 124) # Local variable number, no null check
haslocal.append(124)
def_op('STORE_FAST', 125) # Local variable number
haslocal.append(125)
def_op('DELETE_FAST', 126) # Local variable number
haslocal.append(126)
def_op('LOAD_FAST_CHECK', 127) # Local variable number
haslocal.append(127)
jrel_op('POP_JUMP_FORWARD_IF_NOT_NONE', 128)
jrel_op('POP_JUMP_FORWARD_IF_NONE', 129)
def_op('RAISE_VARARGS', 130) # Number of raise arguments (1, 2, or 3)
Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_dis.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ def bug42562():
--> BINARY_OP 11 (/)
POP_TOP
%3d LOAD_FAST 1 (tb)
%3d LOAD_FAST_CHECK 1 (tb)
RETURN_VALUE
>> PUSH_EXC_INFO
Expand Down Expand Up @@ -1399,7 +1399,7 @@ def _prepare_test_cases():
Instruction(opname='LOAD_CONST', opcode=100, arg=4, argval='I can haz else clause?', argrepr="'I can haz else clause?'", offset=100, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='CALL', opcode=171, arg=1, argval=1, argrepr='', offset=102, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='POP_TOP', opcode=1, arg=None, argval=None, argrepr='', offset=112, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_FAST', opcode=124, arg=0, argval='i', argrepr='i', offset=114, starts_line=11, is_jump_target=True, positions=None),
Instruction(opname='LOAD_FAST_CHECK', opcode=127, arg=0, argval='i', argrepr='i', offset=114, starts_line=11, is_jump_target=True, positions=None),
Instruction(opname='POP_JUMP_FORWARD_IF_FALSE', opcode=114, arg=34, argval=186, argrepr='to 186', offset=116, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_GLOBAL', opcode=116, arg=3, argval='print', argrepr='NULL + print', offset=118, starts_line=12, is_jump_target=True, positions=None),
Instruction(opname='LOAD_FAST', opcode=124, arg=0, argval='i', argrepr='i', offset=130, starts_line=None, is_jump_target=False, positions=None),
Expand Down
180 changes: 180 additions & 0 deletions Lib/test/test_peepholer.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import dis
from itertools import combinations, product
import sys
import textwrap
import unittest

Expand Down Expand Up @@ -682,5 +683,184 @@ def test_bpo_45773_pop_jump_if_false(self):
compile("while True or not spam: pass", "<test>", "exec")


class TestMarkingVariablesAsUnKnown(BytecodeTestCase):

def setUp(self):
self.addCleanup(sys.settrace, sys.gettrace())
sys.settrace(None)

def test_load_fast_known_simple(self):
def f():
x = 1
y = x + x
self.assertInBytecode(f, 'LOAD_FAST')

def test_load_fast_unknown_simple(self):
def f():
if condition():
x = 1
print(x)
self.assertInBytecode(f, 'LOAD_FAST_CHECK')
self.assertNotInBytecode(f, 'LOAD_FAST')

def test_load_fast_unknown_because_del(self):
def f():
x = 1
del x
print(x)
self.assertInBytecode(f, 'LOAD_FAST_CHECK')
self.assertNotInBytecode(f, 'LOAD_FAST')

def test_load_fast_known_because_parameter(self):
def f1(x):
print(x)
self.assertInBytecode(f1, 'LOAD_FAST')
self.assertNotInBytecode(f1, 'LOAD_FAST_CHECK')

def f2(*, x):
print(x)
self.assertInBytecode(f2, 'LOAD_FAST')
self.assertNotInBytecode(f2, 'LOAD_FAST_CHECK')

def f3(*args):
print(args)
self.assertInBytecode(f3, 'LOAD_FAST')
self.assertNotInBytecode(f3, 'LOAD_FAST_CHECK')

def f4(**kwargs):
print(kwargs)
self.assertInBytecode(f4, 'LOAD_FAST')
self.assertNotInBytecode(f4, 'LOAD_FAST_CHECK')

def f5(x=0):
print(x)
self.assertInBytecode(f5, 'LOAD_FAST')
self.assertNotInBytecode(f5, 'LOAD_FAST_CHECK')

def test_load_fast_known_because_already_loaded(self):
def f():
if condition():
x = 1
print(x)
print(x)
self.assertInBytecode(f, 'LOAD_FAST_CHECK')
self.assertInBytecode(f, 'LOAD_FAST')

def test_load_fast_known_multiple_branches(self):
def f():
if condition():
x = 1
else:
x = 2
print(x)
self.assertInBytecode(f, 'LOAD_FAST')
self.assertNotInBytecode(f, 'LOAD_FAST_CHECK')

def test_load_fast_unknown_after_error(self):
def f():
try:
res = 1 / 0
except ZeroDivisionError:
pass
return res
# LOAD_FAST (known) still occurs in the no-exception branch.
# Assert that it doesn't occur in the LOAD_FAST_CHECK branch.
self.assertInBytecode(f, 'LOAD_FAST_CHECK')

def test_load_fast_unknown_after_error_2(self):
def f():
try:
1 / 0
except:
print(a, b, c, d, e, f, g)
a = b = c = d = e = f = g = 1
self.assertInBytecode(f, 'LOAD_FAST_CHECK')
self.assertNotInBytecode(f, 'LOAD_FAST')

def test_setting_lineno_adds_check(self):
code = textwrap.dedent("""\
def f():
x = 2
L = 3
L = 4
for i in range(55):
x + 6
del x
L = 8
L = 9
L = 10
""")
ns = {}
exec(code, ns)
f = ns['f']
self.assertInBytecode(f, "LOAD_FAST")
def trace(frame, event, arg):
if event == 'line' and frame.f_lineno == 9:
frame.f_lineno = 2
sys.settrace(None)
return None
return trace
sys.settrace(trace)
f()
self.assertNotInBytecode(f, "LOAD_FAST")

def make_function_with_no_checks(self):
code = textwrap.dedent("""\
def f():
x = 2
L = 3
L = 4
L = 5
if not L:
x + 7
y = 2
""")
ns = {}
exec(code, ns)
f = ns['f']
self.assertInBytecode(f, "LOAD_FAST")
self.assertNotInBytecode(f, "LOAD_FAST_CHECK")
return f

def test_deleting_local_adds_check(self):
f = self.make_function_with_no_checks()
def trace(frame, event, arg):
if event == 'line' and frame.f_lineno == 4:
del frame.f_locals["x"]
sys.settrace(None)
return None
return trace
sys.settrace(trace)
f()
self.assertNotInBytecode(f, "LOAD_FAST")
self.assertInBytecode(f, "LOAD_FAST_CHECK")

def test_modifying_local_does_not_add_check(self):
f = self.make_function_with_no_checks()
def trace(frame, event, arg):
if event == 'line' and frame.f_lineno == 4:
frame.f_locals["x"] = 42
sys.settrace(None)
return None
return trace
sys.settrace(trace)
f()
self.assertInBytecode(f, "LOAD_FAST")
self.assertNotInBytecode(f, "LOAD_FAST_CHECK")

def test_initializing_local_does_not_add_check(self):
f = self.make_function_with_no_checks()
def trace(frame, event, arg):
if event == 'line' and frame.f_lineno == 4:
frame.f_locals["y"] = 42
sys.settrace(None)
return None
return trace
sys.settrace(trace)
f()
self.assertInBytecode(f, "LOAD_FAST")
self.assertNotInBytecode(f, "LOAD_FAST_CHECK")


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Avoid ``NULL`` checks for uninitialized local variables by determining at compile time which variables must be initialized.
Loading

0 comments on commit f425f3b

Please sign in to comment.