Skip to content

Commit 7ac2792

Browse files
committed
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.
1 parent 7f3ed3e commit 7ac2792

3 files changed

Lines changed: 47 additions & 65 deletions

File tree

src/bytecode/cfg.py

Lines changed: 1 addition & 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,6 @@ 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
845843
nte = instr.copy()
846844
nte.entry = try_begins[active_try_begin][-1]
847845
old_block.append(nte)
@@ -888,7 +886,6 @@ def from_bytecode(bytecode: _bytecode.Bytecode) -> "ControlFlowGraph":
888886
if isinstance(instr, (Instr, TryBegin, TryEnd)):
889887
new = instr.copy()
890888
if isinstance(instr, TryBegin):
891-
assert active_try_begin is None
892889
active_try_begin = instr
893890
try_begin_inserted_in_block = True
894891
assert isinstance(new, TryBegin)
@@ -982,9 +979,7 @@ def to_bytecode(self) -> _bytecode.Bytecode:
982979
# If due to jumps and split TryBegin, we encounter a TryBegin
983980
# while we still have a TryBegin ensure they can be fused.
984981
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
982+
_, byt_tb = last_try_begin
988983
byt_tb.stack_depth = min(
989984
byt_tb.stack_depth, instr.stack_depth
990985
)
@@ -1003,7 +998,6 @@ def to_bytecode(self) -> _bytecode.Bytecode:
1003998
# If we did not yet compute the required stack depth
1004999
# keep the value as UNSET
10051000
if entry.stack_depth is UNSET:
1006-
assert instr.stack_depth is UNSET
10071001
byt_te.entry.stack_depth = UNSET
10081002
else:
10091003
byt_te.entry.stack_depth = min(

src/bytecode/concrete.py

Lines changed: 19 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@
6767
# - dis displays bytes
6868
OFFSET_AS_INSTRUCTION = PY310
6969

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

7177
def _set_docstring(code: _bytecode.BaseBytecode, consts: Sequence) -> None:
7278
if not consts:
@@ -478,9 +484,6 @@ def _assemble_lnotab(
478484
doff = 0
479485
dlineno -= 127
480486

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

486489
return b"".join(lnotab)
@@ -500,7 +503,6 @@ def _pack_linetable(
500503
linetable.append(struct.pack("Bb", 0, 127))
501504
dlineno -= 127
502505

503-
assert -127 <= dlineno <= 127
504506
else:
505507
dlineno = -128
506508

@@ -520,8 +522,6 @@ def _pack_linetable(
520522
else:
521523
linetable.append(struct.pack("Bb", doff, dlineno))
522524

523-
assert 0 <= doff <= 254
524-
525525
# Used on 3.10
526526
def _assemble_linestable(
527527
self,
@@ -640,9 +640,6 @@ def _pack_location(
640640

641641
# We enforce the end_lineno to be defined
642642
else:
643-
assert end_lineno is not None
644-
assert end_col_offset is not None
645-
646643
# Short forms
647644
if (
648645
end_lineno == l_lineno
@@ -812,7 +809,6 @@ def _parse_varint(except_table_iterator: Iterator[int]) -> int:
812809
def _parse_exception_table(
813810
self, exception_table: bytes
814811
) -> List[ExceptionTableEntry]:
815-
assert PY311
816812
table = []
817813
iterator = iter(exception_table)
818814
try:
@@ -833,7 +829,6 @@ def _encode_varint(value: int, set_begin_marker: bool = False) -> Iterator[int]:
833829
# Encode value as a varint on 7 bits (MSB should come first) and set
834830
# the begin marker if requested.
835831
temp: List[int] = []
836-
assert value >= 0
837832
while value:
838833
temp.append(value & 63 | (64 if temp else 0))
839834
value >>= 6
@@ -967,7 +962,6 @@ def to_bytecode(
967962
for entry in self.exception_table:
968963
# Ensure we do not have more than one entry with identical starting
969964
# offsets
970-
assert entry.start_offset not in ex_start
971965
ex_start[entry.start_offset] = entry
972966
ex_end.setdefault(entry.stop_offset, []).append(entry)
973967

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

10511047
# We may need to insert a TryEnd after a CACHE so we need to run the
10521048
# through the last block.
@@ -1055,14 +1051,14 @@ def to_bytecode(
10551051
arg: InstrArg
10561052
c_arg = c_instr.arg
10571053
# FIXME: better error reporting
1058-
if opcode in _opcode.hasconst:
1054+
if opcode in HAS_CONST:
10591055
arg = self.consts[c_arg]
10601056
elif opcode in _opcode.haslocal:
10611057
if opcode in DUAL_ARG_OPCODES:
10621058
arg = (locals_lookup[c_arg >> 4], locals_lookup[c_arg & 15])
10631059
else:
10641060
arg = locals_lookup[c_arg]
1065-
elif opcode in _opcode.hasname:
1061+
elif opcode in HAS_NAME:
10661062
if opcode in BITFLAG_OPCODES:
10671063
arg = (
10681064
bool(c_arg & 1),
@@ -1072,7 +1068,7 @@ def to_bytecode(
10721068
arg = (bool(c_arg & 1), bool(c_arg & 2), self.names[c_arg >> 2])
10731069
else:
10741070
arg = self.names[c_arg]
1075-
elif opcode in _opcode.hasfree:
1071+
elif opcode in HAS_FREE:
10761072
if c_arg < ncells:
10771073
n_or_cell = cells_lookup[c_arg]
10781074
arg = (
@@ -1083,7 +1079,7 @@ def to_bytecode(
10831079
else:
10841080
name = self.freevars[c_arg - ncells]
10851081
arg = FreeVar(name)
1086-
elif opcode in _opcode.hascompare:
1082+
elif opcode in HAS_COMPARE:
10871083
arg = Compare(
10881084
(c_arg >> 5) + ((1 << 4) if (c_arg & 16) else 0)
10891085
if PY313
@@ -1175,7 +1171,6 @@ class _ConvertBytecodeToConcrete:
11751171
_compute_jumps_passes = 10
11761172

11771173
def __init__(self, code: _bytecode.Bytecode) -> None:
1178-
assert isinstance(code, _bytecode.Bytecode)
11791174
self.bytecode = code
11801175

11811176
# temporary variables
@@ -1248,8 +1243,8 @@ def concrete_instructions(self) -> None:
12481243
ConcreteInstr(
12491244
"CACHE", 0, location=self.instructions[-1].location
12501245
)
1251-
for i in range(self.required_caches)
12521246
]
1247+
* self.required_caches
12531248
)
12541249
self.required_caches = 0
12551250
self.seen_manual_cache = False
@@ -1272,7 +1267,6 @@ def concrete_instructions(self) -> None:
12721267

12731268
if isinstance(instr, TryBegin):
12741269
# We expect the stack depth to have be provided or computed earlier
1275-
assert instr.stack_depth is not UNSET
12761270
# NOTE here we store the index of the instruction at which the
12771271
# exception table entry starts. This is not the final value we want,
12781272
# we want the offset in the bytecode but that requires to compute
@@ -1306,16 +1300,10 @@ def concrete_instructions(self) -> None:
13061300
# fake value, real value is set in compute_jumps()
13071301
c_arg = 0
13081302
is_jump = True
1309-
elif opcode in _opcode.hasconst:
1303+
elif opcode in HAS_CONST:
13101304
c_arg = self.add_const(arg)
1311-
elif opcode in _opcode.haslocal:
1305+
elif opcode in HAS_LOCAL:
13121306
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-
)
13191307
arg1_index = self.add(self.varnames, arg[0])
13201308
arg2_index = self.add(self.varnames, arg[1])
13211309
if arg1_index > 16 or arg2_index > 16:
@@ -1335,7 +1323,7 @@ def concrete_instructions(self) -> None:
13351323
else:
13361324
assert isinstance(arg, str)
13371325
c_arg = self.add(self.varnames, arg)
1338-
elif opcode in _opcode.hasname:
1326+
elif opcode in HAS_NAME:
13391327
if opcode in BITFLAG_OPCODES:
13401328
assert (
13411329
isinstance(arg, tuple)
@@ -1362,15 +1350,14 @@ def concrete_instructions(self) -> None:
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:
1370-
assert isinstance(arg, FreeVar)
13711358
free_instrs.append(len(self.instructions))
13721359
c_arg = self.bytecode.freevars.index(arg.name)
1373-
elif opcode in _opcode.hascompare:
1360+
elif opcode in HAS_COMPARE:
13741361
if isinstance(arg, Compare):
13751362
# In Python 3.13 the 4 lowest bits are used for caching
13761363
# 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)