8000 gh-93143: Avoid NULL check in LOAD_FAST based on analysis in the comp… · python/cpython@f425f3b · GitHub
[go: up one dir, main page]

Skip to content

Commit f425f3b

Browse files
authored
gh-93143: Avoid NULL check in LOAD_FAST based on analysis in the compiler (GH-93144)
1 parent 8a5e3c2 commit f425f3b

File tree

11 files changed

+371
-52
lines changed

11 files changed

+371
-52
lines changed

Include/internal/pycore_opcode.h

Lines changed: 10 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/opcode.h

Lines changed: 16 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/importlib/_bootstrap_external.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,7 @@ def _write_atomic(path, data, mode=0o666):
406406

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

410411
# Python 3.13 will start with 3550
411412

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

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

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

Lib/opcode.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,14 @@ def jabs_op(name, op):
139139
def_op('COPY', 120)
140140
def_op('BINARY_OP', 122)
141141
jrel_op('SEND', 123) # Number of bytes to skip
142-
def_op('LOAD_FAST', 124) # Local variable number
142+
def_op('LOAD_FAST', 124) # Local variable number, no null check
143143
haslocal.append(124)
144144
def_op('STORE_FAST', 125) # Local variable number
145145
haslocal.append(125)
146146
def_op('DELETE_FAST', 126) # Local variable number
147147
haslocal.append(126)
148+
def_op('LOAD_FAST_CHECK', 127) # Local variable number
149+
haslocal.append(127)
148150
jrel_op('POP_JUMP_FORWARD_IF_NOT_NONE', 128)
149151
jrel_op('POP_JUMP_FORWARD_IF_NONE', 129)
150152
def_op('RAISE_VARARGS', 130) # Number of raise arguments (1, 2, or 3)

Lib/test/test_dis.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ def bug42562():
360360
--> BINARY_OP 11 (/)
361361
POP_TOP
362362
363-
%3d LOAD_FAST 1 (tb)
363+
%3d LOAD_FAST_CHECK 1 (tb)
364364
RETURN_VALUE
365365
>> PUSH_EXC_INFO
366366
@@ -1399,7 +1399,7 @@ def _prepare_test_cases():
13991399
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),
14001400
Instruction(opname='CALL', opcode=171, arg=1, argval=1, argrepr='', offset=102, starts_line=None, is_jump_target=False, positions=None),
14011401
Instruction(opname='POP_TOP', opcode=1, arg=None, argval=None, argrepr='', offset=112, starts_line=None, is_jump_target=False, positions=None),
1402-
Instruction(opname='LOAD_FAST', opcode=124, arg=0, argval='i', argrepr='i', offset=114, starts_line=11, is_jump_target=True, positions=None),
1402+
Instruction(opname='LOAD_FAST_CHECK', opcode=127, arg=0, argval='i', argrepr='i', offset=114, starts_line=11, is_jump_target=True, positions=None),
14031403
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),
14041404
Instruction(opname='LOAD_GLOBAL', opcode=116, arg=3, argval='print', argrepr='NULL + print', offset=118, starts_line=12, is_jump_target=True, positions=None),
14051405
Instruction(opname='LOAD_FAST', opcode=124, arg=0, argval='i', argrepr='i', offset=130, starts_line=None, is_jump_target=False, positions=None),

Lib/test/test_peepholer.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import dis
22
from itertools import combinations, product
3+
import sys
34
import textwrap
45
import unittest
56

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

684685

686+
class TestMarkingVariablesAsUnKnown(BytecodeTestCase):
687+
688+
def setUp(self):
689+
self.addCleanup(sys.settrace, sys.gettrace())
690+
sys.settrace(None)
691+
692+
def test_load_fast_known_simple(self):
693+
def f():
694+
x = 1
695+
y = x + x
696+
self.assertInBytecode(f, 'LOAD_FAST')
697+
698+
def test_load_fast_unknown_simple(self):
699+
def f():
700+
if condition():
701+
x = 1
702+
print(x)
703+
self.assertInBytecode(f, 'LOAD_FAST_CHECK')
704+
self.assertNotInBytecode(f, 'LOAD_FAST')
705+
706+
def test_load_fast_unknown_because_del(self):
707+
def f():
708+
x = 1
709+
del x
710+
print(x)
711+
self.assertInBytecode(f, 'LOAD_FAST_CHECK')
712+
self.assertNotInBytecode(f, 'LOAD_FAST')
713+
714+
def test_load_fast_known_because_parameter(self):
715+
def f1(x):
716+
print(x)
717+
self.assertInBytecode(f1, 'LOAD_FAST')
718+
self.assertNotInBytecode(f1, 'LOAD_FAST_CHECK')
719+
720+
def f2(*, x):
721+
print(x)
722+
self.assertInBytecode(f2, 'LOAD_FAST')
723+
self.assertNotInBytecode(f2, 'LOAD_FAST_CHECK')
724+
725+
def f3(*args):
726+
print(args)
727+
self.assertInBytecode(f3, 'LOAD_FAST')
728+
self.assertNotInBytecode(f3, 'LOAD_FAST_CHECK')
729+
730+
def f4(**kwargs):
731+
print(kwargs)
732+
self.assertInBytecode(f4, 'LOAD_FAST')
733+
self.assertNotInBytecode(f4, 'LOAD_FAST_CHECK')
734+
735+
def f5(x=0):
736+
print(x)
737+
self.assertInBytecode(f5, 'LOAD_FAST')
738+
self.assertNotInBytecode(f5, 'LOAD_FAST_CHECK')
739+
740+
def test_load_fast_known_because_already_loaded(self):
741+
def f():
742+
if condition():
743+
x = 1
744+
print(x)
745+
print(x)
746+
self.assertInBytecode(f, 'LOAD_FAST_CHECK')
747+
self.assertInBytecode(f, 'LOAD_FAST')
748+
749+
def test_load_fast_known_multiple_branches(self):
750+
def f():
751+
if condition():
752+
x = 1
753+
else:
754+
x = 2
755+
print(x)
756+
self.assertInBytecode(f, 'LOAD_FAST')
757+
self.assertNotInBytecode(f, 'LOAD_FAST_CHECK')
758+
759+
def test_load_fast_unknown_after_error(self):
760+
def f():
761+
try:
762+
res = 1 / 0
763+
except ZeroDivisionError:
764+
pass
765+
return res
766+
# LOAD_FAST (known) still occurs in the no-exception branch.
767+
# Assert that it doesn't occur in the LOAD_FAST_CHECK branch.
768+
self.assertInBytecode(f, 'LOAD_FAST_CHECK')
769+
770+
def test_load_fast_unknown_after_error_2(self):
771+
def f():
772+
try:
773+
1 / 0
774+
except:
775+
print(a, b, c, d, e, f, g)
776+
a = b = c = d = e = f = g = 1
777+
self.assertInBytecode(f, 'LOAD_FAST_CHECK')
778+
self.assertNotInBytecode(f, 'LOAD_FAST')
779+
780+
def test_setting_lineno_adds_check(self):
781+
code = textwrap.dedent("""\
782+
def f():
783+
x = 2
784+
L = 3
785+
L = 4
786+
for i in range(55):
787+
x + 6
788+
del x
789+
L = 8
790+
L = 9
791+
L = 10
792+
""")
793+
ns = {}
794+
exec(code, ns)
795+
f = ns['f']
796+
self.assertInBytecode(f, "LOAD_FAST")
797+
def trace(frame, event, arg):
798+
if event == 'line' and frame.f_lineno == 9:
799+
frame.f_lineno = 2
800+
sys.settrace(None)
801+
return None
802+
return trace
803+
sys.settrace(trace)
804+
f()
805+
self.assertNotInBytecode(f, "LOAD_FAST")
806+
807+
def make_function_with_no_checks(self):
808+
code = textwrap.dedent("""\
809+
def f():
810+
x = 2
811+
L = 3
812+
L = 4
813+
L = 5
814+
if not L:
815+
x + 7
816+
y = 2
817+
""")
818+
ns = {}
819+
exec(code, ns)
820+
f = ns['f']
821+
self.assertInBytecode(f, "LOAD_FAST")
822+
self.assertNotInBytecode(f, "LOAD_FAST_CHECK")
823+
return f
824+
825+
def test_deleting_local_adds_check(self):
826+
f = self.make_function_with_no_checks()
827+
def trace(frame, event, arg):
828+
if event == 'line' and frame.f_lineno == 4:
829+
del frame.f_locals["x"]
830+
sys.settrace(None)
831+
return None
832+
return trace
833+
sys.settrace(trace)
834+
f()
835+
self.assertNotInBytecode(f, "LOAD_FAST")
836+
self.assertInBytecode(f, "LOAD_FAST_CHECK")
837+
838+
def test_modifying_local_does_not_add_check(self):
839+
f = self.make_function_with_no_checks()
840+
def trace(frame, event, arg):
841+
if event == 'line' and frame.f_lineno == 4:
842+
frame.f_locals["x"] = 42
843+
sys.settrace(None)
844+
return None
845+
return trace
846+
sys.settrace(trace)
847+
f()
848+
self.assertInBytecode(f, "LOAD_FAST")
849+
self.assertNotInBytecode(f, "LOAD_FAST_CHECK")
850+
851+
def test_initializing_local_does_not_add_check(self):
852+
f = self.make_function_with_no_checks()
853+
def trace(frame, event, arg):
854+
if event == 'line' and frame.f_lineno == 4:
855+
frame.f_locals["y"] = 42
856+
sys.settrace(None)
857+
return None
858+
return trace
859+
sys.settrace(trace)
860+
f()
861+
self.assertInBytecode(f, "LOAD_FAST")
862+
self.assertNotInBytecode(f, "LOAD_FAST_CHECK")
863+
864+
685865
if __name__ == "__main__":
686866
unittest.main()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Avoid ``NULL`` checks for uninitialized local variables by determining at compile time which variables must be initialized.

0 commit comments

Comments
 (0)
0