Skip to content

Commit 317dd0f

Browse files
perf: improve general performance (MatthieuDartiailh#172)
* perf: improve general performance We try to use sets for quick lookups and cache the result of some repetitive operations to speed up some operations. We also remove a good deal of asserts embedded within the code that adde extra overhead. * fix linting --------- Co-authored-by: Matthieu Dartiailh <m.dartiailh@gmail.com>
1 parent 7b6825f commit 317dd0f

3 files changed

Lines changed: 57 additions & 73 deletions

File tree

src/bytecode/cfg.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -588,7 +588,6 @@ def compute_stacksize(
588588
if compute_exception_stack_depths:
589589
for tb in common.try_begins:
590590
size = common.exception_block_startsize[id(tb.target)]
591-
assert size >= 0
592591
tb.stack_depth = size
593592

594593
return args
@@ -841,7 +840,7 @@ def from_bytecode(bytecode: _bytecode.Bytecode) -> "ControlFlowGraph":
841840
# The last instruction is final, if the current instruction is a
842841
# TryEnd insert it in the same block and move to the next instruction
843842
if last_instr.is_final() and isinstance(instr, TryEnd):
844-
assert active_try_begin
843+
assert active_try_begin is not None
845844
nte = instr.copy()
846845
nte.entry = try_begins[active_try_begin][-1]
847846
old_block.append(nte)
@@ -888,7 +887,6 @@ def from_bytecode(bytecode: _bytecode.Bytecode) -> "ControlFlowGraph":
888887
if isinstance(instr, (Instr, TryBegin, TryEnd)):
889888
new = instr.copy()
890889
if isinstance(instr, TryBegin):
891-
assert active_try_begin is None
892890
active_try_begin = instr
893891
try_begin_inserted_in_block = True
894892
assert isinstance(new, TryBegin)
@@ -982,9 +980,7 @@ def to_bytecode(self) -> _bytecode.Bytecode:
982980
# If due to jumps and split TryBegin, we encounter a TryBegin
983981
# while we still have a TryBegin ensure they can be fused.
984982
if last_try_begin is not None:
985-
cfg_tb, byt_tb = last_try_begin
986-
assert instr.target is cfg_tb.target
987-
assert instr.push_lasti == cfg_tb.push_lasti
983+
_, byt_tb = last_try_begin
988984
byt_tb.stack_depth = min(
989985
byt_tb.stack_depth, instr.stack_depth
990986
)
@@ -1003,7 +999,6 @@ def to_bytecode(self) -> _bytecode.Bytecode:
1003999
# If we did not yet compute the required stack depth
10041000
# keep the value as UNSET
10051001
if entry.stack_depth is UNSET:
1006-
assert instr.stack_depth is UNSET
10071002
byt_te.entry.stack_depth = UNSET
10081003
else:
10091004
byt_te.entry.stack_depth = min(

src/bytecode/concrete.py

Lines changed: 28 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
Type,
2020
TypeVar,
2121
Union,
22+
cast,
2223
)
2324

2425
# alias to keep the 'bytecode' variable free
@@ -67,6 +68,12 @@
6768
# - dis displays bytes
6869
OFFSET_AS_INSTRUCTION = PY310
6970

71+
HAS_CONST = set(_opcode.hasconst)
72+
HAS_LOCAL = set(_opcode.haslocal)
73+
HAS_NAME = set(_opcode.hasname)
74+
HAS_FREE = set(_opcode.hasfree)
75+
HAS_COMPARE = set(_opcode.hascompare)
76+
7077

7178
def _set_docstring(code: _bytecode.BaseBytecode, consts: Sequence) -> None:
7279
if not consts:
@@ -478,9 +485,6 @@ def _assemble_lnotab(
478485
doff = 0
479486
dlineno -= 127
480487

481-
assert 0 <= doff <= 255
482-
assert -128 <= dlineno <= 127
483-
484488
lnotab.append(struct.pack("Bb", doff, dlineno))
485489

486490
return b"".join(lnotab)
@@ -500,7 +504,6 @@ def _pack_linetable(
500504
linetable.append(struct.pack("Bb", 0, 127))
501505
dlineno -= 127
502506

503-
assert -127 <= dlineno <= 127
504507
else:
505508
dlineno = -128
506509

@@ -520,8 +523,6 @@ def _pack_linetable(
520523
else:
521524
linetable.append(struct.pack("Bb", doff, dlineno))
522525

523-
assert 0 <= doff <= 254
524-
525526
# Used on 3.10
526527
def _assemble_linestable(
527528
self,
@@ -640,7 +641,6 @@ def _pack_location(
640641

641642
# We enforce the end_lineno to be defined
642643
else:
643-
assert end_lineno is not None
644644
assert end_col_offset is not None
645645

646646
# Short forms
@@ -674,6 +674,8 @@ def _pack_location(
674674

675675
# Long form
676676
else:
677+
assert end_lineno is not None
678+
677679
packed.extend(
678680
(
679681
self._pack_location_header(14, size),
@@ -812,7 +814,6 @@ def _parse_varint(except_table_iterator: Iterator[int]) -> int:
812814
def _parse_exception_table(
813815
self, exception_table: bytes
814816
) -> List[ExceptionTableEntry]:
815-
assert PY311
816817
table = []
817818
iterator = iter(exception_table)
818819
try:
@@ -833,7 +834,6 @@ def _encode_varint(value: int, set_begin_marker: bool = False) -> Iterator[int]:
833834
# Encode value as a varint on 7 bits (MSB should come first) and set
834835
# the begin marker if requested.
835836
temp: List[int] = []
836-
assert value >= 0
837837
while value:
838838
temp.append(value & 63 | (64 if temp else 0))
839839
value >>= 6
@@ -967,7 +967,6 @@ def to_bytecode(
967967
for entry in self.exception_table:
968968
# Ensure we do not have more than one entry with identical starting
969969
# offsets
970-
assert entry.start_offset not in ex_start
971970
ex_start[entry.start_offset] = entry
972971
ex_end.setdefault(entry.stop_offset, []).append(entry)
973972

@@ -1046,7 +1045,9 @@ def to_bytecode(
10461045
# We are careful to first advance the offset and check that the CACHE
10471046
# is not a jump target. It should never be the case but we double check.
10481047
if prune_caches and c_instr.name == "CACHE":
1049-
assert jump_target is None
1048+
if jump_target is not None:
1049+
msg = "cache instruction cannot have jump target"
1050+
raise ValueError(msg)
10501051

10511052
# We may need to insert a TryEnd after a CACHE so we need to run the
10521053
# through the last block.
@@ -1055,14 +1056,14 @@ def to_bytecode(
10551056
arg: InstrArg
10561057
c_arg = c_instr.arg
10571058
# FIXME: better error reporting
1058-
if opcode in _opcode.hasconst:
1059+
if opcode in HAS_CONST:
10591060
arg = self.consts[c_arg]
10601061
elif opcode in _opcode.haslocal:
10611062
if opcode in DUAL_ARG_OPCODES:
10621063
arg = (locals_lookup[c_arg >> 4], locals_lookup[c_arg & 15])
10631064
else:
10641065
arg = locals_lookup[c_arg]
1065-
elif opcode in _opcode.hasname:
1066+
elif opcode in HAS_NAME:
10661067
if opcode in BITFLAG_OPCODES:
10671068
arg = (
10681069
bool(c_arg & 1),
@@ -1072,7 +1073,7 @@ def to_bytecode(
10721073
arg = (bool(c_arg & 1), bool(c_arg & 2), self.names[c_arg >> 2])
10731074
else:
10741075
arg = self.names[c_arg]
1075-
elif opcode in _opcode.hasfree:
1076+
elif opcode in HAS_FREE:
10761077
if c_arg < ncells:
10771078
n_or_cell = cells_lookup[c_arg]
10781079
arg = (
@@ -1083,7 +1084,7 @@ def to_bytecode(
10831084
else:
10841085
name = self.freevars[c_arg - ncells]
10851086
arg = FreeVar(name)
1086-
elif opcode in _opcode.hascompare:
1087+
elif opcode in HAS_COMPARE:
10871088
arg = Compare(
10881089
(c_arg >> 5) + ((1 << 4) if (c_arg & 16) else 0)
10891090
if PY313
@@ -1175,7 +1176,6 @@ class _ConvertBytecodeToConcrete:
11751176
_compute_jumps_passes = 10
11761177

11771178
def __init__(self, code: _bytecode.Bytecode) -> None:
1178-
assert isinstance(code, _bytecode.Bytecode)
11791179
self.bytecode = code
11801180

11811181
# temporary variables
@@ -1248,8 +1248,8 @@ def concrete_instructions(self) -> None:
12481248
ConcreteInstr(
12491249
"CACHE", 0, location=self.instructions[-1].location
12501250
)
1251-
for i in range(self.required_caches)
12521251
]
1252+
* self.required_caches
12531253
)
12541254
self.required_caches = 0
12551255
self.seen_manual_cache = False
@@ -1272,7 +1272,6 @@ def concrete_instructions(self) -> None:
12721272

12731273
if isinstance(instr, TryBegin):
12741274
# We expect the stack depth to have be provided or computed earlier
1275-
assert instr.stack_depth is not UNSET
12761275
# NOTE here we store the index of the instruction at which the
12771276
# exception table entry starts. This is not the final value we want,
12781277
# we want the offset in the bytecode but that requires to compute
@@ -1306,18 +1305,13 @@ def concrete_instructions(self) -> None:
13061305
# fake value, real value is set in compute_jumps()
13071306
c_arg = 0
13081307
is_jump = True
1309-
elif opcode in _opcode.hasconst:
1308+
elif opcode in HAS_CONST:
13101309
c_arg = self.add_const(arg)
1311-
elif opcode in _opcode.haslocal:
1310+
elif opcode in HAS_LOCAL:
13121311
if opcode in DUAL_ARG_OPCODES:
1313-
assert (
1314-
isinstance(arg, tuple)
1315-
and len(arg) == 2
1316-
and isinstance(arg[0], str)
1317-
and isinstance(arg[1], str)
1318-
)
1319-
arg1_index = self.add(self.varnames, arg[0])
1320-
arg2_index = self.add(self.varnames, arg[1])
1312+
_arg2 = cast(Tuple[str, str], arg)
1313+
arg1_index = self.add(self.varnames, _arg2[0])
1314+
arg2_index = self.add(self.varnames, _arg2[1])
13211315
if arg1_index > 16 or arg2_index > 16:
13221316
n1, n2 = DUAL_ARG_OPCODES_SINGLE_OPS[opcode]
13231317
c_instr = ConcreteInstr(n1, arg1_index, location=location)
@@ -1335,7 +1329,7 @@ def concrete_instructions(self) -> None:
13351329
else:
13361330
assert isinstance(arg, str)
13371331
c_arg = self.add(self.varnames, arg)
1338-
elif opcode in _opcode.hasname:
1332+
elif opcode in HAS_NAME:
13391333
if opcode in BITFLAG_OPCODES:
13401334
assert (
13411335
isinstance(arg, tuple)
@@ -1350,27 +1344,21 @@ def concrete_instructions(self) -> None:
13501344
assert False, arg # noqa
13511345
c_arg = int(arg[0]) + (index << 1)
13521346
elif opcode in BITFLAG2_OPCODES:
1353-
assert (
1354-
isinstance(arg, tuple)
1355-
and len(arg) == 3
1356-
and isinstance(arg[0], bool)
1357-
and isinstance(arg[1], bool)
1358-
and isinstance(arg[2], str)
1359-
), arg
1360-
index = self.add(self.names, arg[2])
1361-
c_arg = int(arg[0]) + 2 * int(arg[1]) + (index << 2)
1347+
_arg3 = cast(Tuple[bool, bool, str], arg)
1348+
index = self.add(self.names, _arg3[2])
1349+
c_arg = int(_arg3[0]) + 2 * int(_arg3[1]) + (index << 2)
13621350
else:
13631351
assert isinstance(arg, str), f"Got {arg}, expected a str"
13641352
c_arg = self.add(self.names, arg)
1365-
elif opcode in _opcode.hasfree:
1353+
elif opcode in HAS_FREE:
13661354
if isinstance(arg, CellVar):
13671355
cell_instrs.append(len(self.instructions))
13681356
c_arg = self.bytecode.cellvars.index(arg.name)
13691357
else:
13701358
assert isinstance(arg, FreeVar)
13711359
free_instrs.append(len(self.instructions))
13721360
c_arg = self.bytecode.freevars.index(arg.name)
1373-
elif opcode in _opcode.hascompare:
1361+
elif opcode in HAS_COMPARE:
13741362
if isinstance(arg, Compare):
13751363
# In Python 3.13 the 4 lowest bits are used for caching
13761364
# and the 5th one indicate a cast to bool

src/bytecode/instr.py

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
import sys
55
from abc import abstractmethod
66
from dataclasses import dataclass
7+
from functools import cache
78
from marshal import dumps as _dumps
8-
from typing import Any, Callable, Dict, Generic, Optional, Tuple, TypeVar, Union
9+
from typing import Any, Callable, Dict, Generic, Optional, Set, Tuple, TypeVar, Union
910

1011
try:
1112
from typing import TypeGuard
@@ -22,68 +23,68 @@
2223
# Instructions relying on a bit to modify its behavior.
2324
# The lowest bit is used to encode custom behavior.
2425
BITFLAG_OPCODES = (
25-
(
26+
{
2627
_opcode.opmap["BUILD_INTERPOLATION"],
2728
_opcode.opmap["LOAD_GLOBAL"],
2829
_opcode.opmap["LOAD_ATTR"],
29-
)
30+
}
3031
if PY314
3132
else (
32-
(_opcode.opmap["LOAD_GLOBAL"], _opcode.opmap["LOAD_ATTR"])
33+
{_opcode.opmap["LOAD_GLOBAL"], _opcode.opmap["LOAD_ATTR"]}
3334
if PY312
34-
else ((_opcode.opmap["LOAD_GLOBAL"],) if PY311 else ())
35+
else ({_opcode.opmap["LOAD_GLOBAL"]} if PY311 else set())
3536
)
3637
)
3738

38-
BITFLAG2_OPCODES = (_opcode.opmap["LOAD_SUPER_ATTR"],) if PY312 else ()
39+
BITFLAG2_OPCODES = {_opcode.opmap["LOAD_SUPER_ATTR"]} if PY312 else set()
3940

4041
# Binary op opcode which has a dedicated arg
41-
BINARY_OPS = (_opcode.opmap["BINARY_OP"],) if PY311 else ()
42+
BINARY_OPS = {_opcode.opmap["BINARY_OP"]} if PY311 else set()
4243

4344
# Intrinsic related opcodes
44-
INTRINSIC_1OP = (_opcode.opmap["CALL_INTRINSIC_1"],) if PY312 else ()
45-
INTRINSIC_2OP = (_opcode.opmap["CALL_INTRINSIC_2"],) if PY312 else ()
46-
INTRINSIC = INTRINSIC_1OP + INTRINSIC_2OP
45+
INTRINSIC_1OP = {_opcode.opmap["CALL_INTRINSIC_1"]} if PY312 else set()
46+
INTRINSIC_2OP = {_opcode.opmap["CALL_INTRINSIC_2"]} if PY312 else set()
47+
INTRINSIC = INTRINSIC_1OP | INTRINSIC_2OP
4748

4849
# Small integer related opcode
49-
SMALL_INT_OPS = (_opcode.opmap["LOAD_SMALL_INT"],) if PY314 else ()
50+
SMALL_INT_OPS = {_opcode.opmap["LOAD_SMALL_INT"]} if PY314 else set()
5051

5152
# Special method loading related opcodes
52-
SPECIAL_OPS = (_opcode.opmap["LOAD_SPECIAL"],) if PY314 else ()
53+
SPECIAL_OPS = {_opcode.opmap["LOAD_SPECIAL"]} if PY314 else set()
5354

5455
# Common constant loading related opcodes
55-
COMMON_CONSTANT_OPS = (_opcode.opmap["LOAD_COMMON_CONSTANT"],) if PY314 else ()
56+
COMMON_CONSTANT_OPS = {_opcode.opmap["LOAD_COMMON_CONSTANT"]} if PY314 else set()
5657

5758
# Value formatting related opcodes (only handle CONVERT_VALUE and BUILD_INTERPOLATION)
5859
FORMAT_VALUE_OPS = (
59-
(
60+
{
6061
_opcode.opmap["CONVERT_VALUE"],
6162
_opcode.opmap["BUILD_INTERPOLATION"],
62-
)
63+
}
6364
if PY314
64-
else ((_opcode.opmap["CONVERT_VALUE"],) if PY313 else ())
65+
else ({_opcode.opmap["CONVERT_VALUE"]} if PY313 else set())
6566
)
6667

67-
HASJABS = () if PY313 else _opcode.hasjabs
68+
HASJABS = set() if PY313 else set(_opcode.hasjabs)
6869
if sys.version_info >= (3, 13):
69-
HASJREL = _opcode.hasjump
70+
HASJREL = set(_opcode.hasjump)
7071
else:
71-
HASJREL = _opcode.hasjrel
72+
HASJREL = set(_opcode.hasjrel)
7273

7374
#: Opcodes taking 2 arguments (highest 4 bits and lowest 4 bits)
74-
DUAL_ARG_OPCODES: Tuple[int, ...] = ()
75+
DUAL_ARG_OPCODES: Set[int] = set()
7576
DUAL_ARG_OPCODES_SINGLE_OPS: Dict[int, Tuple[str, str]] = {}
7677
if PY313:
77-
DUAL_ARG_OPCODES = (
78+
DUAL_ARG_OPCODES = {
7879
_opcode.opmap["LOAD_FAST_LOAD_FAST"],
7980
_opcode.opmap["STORE_FAST_LOAD_FAST"],
8081
_opcode.opmap["STORE_FAST_STORE_FAST"],
81-
)
82+
}
8283
if PY314:
83-
DUAL_ARG_OPCODES = (
84+
DUAL_ARG_OPCODES = {
8485
*DUAL_ARG_OPCODES,
8586
_opcode.opmap["LOAD_FAST_BORROW_LOAD_FAST_BORROW"],
86-
)
87+
}
8788
DUAL_ARG_OPCODES_SINGLE_OPS = {
8889
_opcode.opmap["LOAD_FAST_LOAD_FAST"]: ("LOAD_FAST", "LOAD_FAST"),
8990
_opcode.opmap["STORE_FAST_LOAD_FAST"]: ("STORE_FAST", "LOAD_FAST"),
@@ -345,11 +346,13 @@ def _check_arg_int(arg: Any, name: str) -> TypeGuard[int]:
345346

346347
if sys.version_info >= (3, 12):
347348

349+
@cache
348350
def opcode_has_argument(opcode: int) -> bool:
349351
return opcode in dis.hasarg
350352

351353
else:
352354

355+
@cache
353356
def opcode_has_argument(opcode: int) -> bool:
354357
return opcode >= dis.HAVE_ARGUMENT
355358

@@ -727,11 +730,9 @@ def stack_effect(self, jump: Optional[bool] = None) -> int:
727730
# 3.12 does the same for LOAD_ATTR
728731
# 3.14 does this for BUILD_INTERPOLATION
729732
elif self._opcode in BITFLAG_OPCODES and isinstance(self._arg, tuple):
730-
assert len(self._arg) == 2
731733
arg = self._arg[0]
732734
# 3.12 does a similar trick for LOAD_SUPER_ATTR
733735
elif self._opcode in BITFLAG2_OPCODES and isinstance(self._arg, tuple):
734-
assert len(self._arg) == 3
735736
arg = self._arg[0]
736737
elif not isinstance(self._arg, int) or self._opcode in _opcode.hasconst:
737738
# Argument is either a non-integer or an integer constant,

0 commit comments

Comments
 (0)