Skip to content

Commit d1eec34

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 c2d97b6 commit d1eec34

3 files changed

Lines changed: 36 additions & 68 deletions

File tree

src/bytecode/cfg.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def __iter__(self) -> Iterator[Union[Instr, SetLineno, TryBegin, TryEnd]]:
5757
isinstance(self[i], Instr) for i in range(index, len(self))
5858
):
5959
raise ValueError(
60-
"Only the last instruction of a basic " "block can be a jump"
60+
"Only the last instruction of a basic block can be a jump"
6161
)
6262

6363
if not isinstance(instr.arg, BasicBlock):
@@ -579,7 +579,6 @@ def compute_stacksize(
579579
if compute_exception_stack_depths:
580580
for tb in common.try_begins:
581581
size = common.exception_block_startsize[id(tb.target)]
582-
assert size >= 0
583582
tb.stack_depth = size
584583

585584
return args
@@ -825,7 +824,6 @@ def from_bytecode(bytecode: _bytecode.Bytecode) -> "ControlFlowGraph":
825824
# The last instruction is final, if the current instruction is a
826825
# TryEnd insert it in the same block and move to the next instruction
827826
if last_instr.is_final() and isinstance(instr, TryEnd):
828-
assert active_try_begin
829827
nte = instr.copy()
830828
nte.entry = try_begins[active_try_begin][-1]
831829
old_block.append(nte)
@@ -874,7 +872,6 @@ def from_bytecode(bytecode: _bytecode.Bytecode) -> "ControlFlowGraph":
874872
if isinstance(instr, (Instr, TryBegin, TryEnd)):
875873
new = instr.copy()
876874
if isinstance(instr, TryBegin):
877-
assert active_try_begin is None
878875
active_try_begin = instr
879876
try_begin_inserted_in_block = True
880877
assert isinstance(new, TryBegin)
@@ -968,9 +965,7 @@ def to_bytecode(self) -> _bytecode.Bytecode:
968965
# If due to jumps and split TryBegin, we encounter a TryBegin
969966
# while we still have a TryBegin ensure they can be fused.
970967
if last_try_begin is not None:
971-
cfg_tb, byt_tb = last_try_begin
972-
assert instr.target is cfg_tb.target
973-
assert instr.push_lasti == cfg_tb.push_lasti
968+
_, byt_tb = last_try_begin
974969
byt_tb.stack_depth = min(
975970
byt_tb.stack_depth, instr.stack_depth
976971
)
@@ -989,7 +984,6 @@ def to_bytecode(self) -> _bytecode.Bytecode:
989984
# If we did not yet compute the required stack depth
990985
# keep the value as UNSET
991986
if entry.stack_depth is UNSET:
992-
assert instr.stack_depth is UNSET
993987
byt_te.entry.stack_depth = UNSET
994988
else:
995989
byt_te.entry.stack_depth = min(

src/bytecode/concrete.py

Lines changed: 20 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@
5555
# - dis displays bytes
5656
OFFSET_AS_INSTRUCTION = sys.version_info >= (3, 10)
5757

58+
HAS_CONST = set(_opcode.hasconst)
59+
HAS_LOCAL = set(_opcode.haslocal)
60+
HAS_NAME = set(_opcode.hasname)
61+
HAS_FREE = set(_opcode.hasfree)
62+
HAS_COMPARE = set(_opcode.hascompare)
63+
5864

5965
def _set_docstring(code: _bytecode.BaseBytecode, consts: Sequence) -> None:
6066
if not consts:
@@ -458,9 +464,6 @@ def _assemble_lnotab(
458464
doff = 0
459465
dlineno -= 127
460466

461-
assert 0 <= doff <= 255
462-
assert -128 <= dlineno <= 127
463-
464467
lnotab.append(struct.pack("Bb", doff, dlineno))
465468

466469
return b"".join(lnotab)
@@ -480,7 +483,6 @@ def _pack_linetable(
480483
linetable.append(struct.pack("Bb", 0, 127))
481484
dlineno -= 127
482485

483-
assert -127 <= dlineno <= 127
484486
else:
485487
dlineno = -128
486488

@@ -500,8 +502,6 @@ def _pack_linetable(
500502
else:
501503
linetable.append(struct.pack("Bb", doff, dlineno))
502504

503-
assert 0 <= doff <= 254
504-
505505
# Used on 3.10
506506
def _assemble_linestable(
507507
self,
@@ -620,9 +620,6 @@ def _pack_location(
620620

621621
# We enforce the end_lineno to be defined
622622
else:
623-
assert end_lineno is not None
624-
assert end_col_offset is not None
625-
626623
# Short forms
627624
if (
628625
end_lineno == l_lineno
@@ -792,7 +789,6 @@ def _parse_varint(except_table_iterator: Iterator[int]) -> int:
792789
def _parse_exception_table(
793790
self, exception_table: bytes
794791
) -> List[ExceptionTableEntry]:
795-
assert sys.version_info >= (3, 11)
796792
table = []
797793
iterator = iter(exception_table)
798794
try:
@@ -813,7 +809,6 @@ def _encode_varint(value: int, set_begin_marker: bool = False) -> Iterator[int]:
813809
# Encode value as a varint on 7 bits (MSB should come first) and set
814810
# the begin marker if requested.
815811
temp: List[int] = []
816-
assert value >= 0
817812
while value:
818813
temp.append(value & 63 | (64 if temp else 0))
819814
value >>= 6
@@ -949,7 +944,6 @@ def to_bytecode(
949944
for entry in self.exception_table:
950945
# Ensure we do not have more than one entry with identical starting
951946
# offsets
952-
assert entry.start_offset not in ex_start
953947
ex_start[entry.start_offset] = entry
954948
ex_end.setdefault(entry.stop_offset, []).append(entry)
955949

@@ -1018,33 +1012,35 @@ def to_bytecode(
10181012
# We are careful to first advance the offset and check that the CACHE
10191013
# is not a jump target. It should never be the case but we double check.
10201014
if prune_caches and c_instr.name == "CACHE":
1021-
assert jump_target is None
1015+
if jump_target is not None:
1016+
msg = "cache instruction cannot have jump target"
1017+
raise ValueError(msg)
10221018

10231019
# We may need to insert a TryEnd after a CACHE so we need to run the
10241020
# through the last block.
10251021
else:
10261022
arg: InstrArg
10271023
c_arg = c_instr.arg
10281024
# FIXME: better error reporting
1029-
if c_instr.opcode in _opcode.hasconst:
1025+
if c_instr.opcode in HAS_CONST:
10301026
arg = self.consts[c_arg]
1031-
elif c_instr.opcode in _opcode.haslocal:
1027+
elif c_instr.opcode in HAS_LOCAL:
10321028
arg = self.varnames[c_arg]
1033-
elif c_instr.opcode in _opcode.hasname:
1029+
elif c_instr.opcode in HAS_NAME:
10341030
if c_instr.name in BITFLAG_INSTRUCTIONS:
10351031
arg = (bool(c_arg & 1), self.names[c_arg >> 1])
10361032
elif c_instr.name in BITFLAG2_INSTRUCTIONS:
10371033
arg = (bool(c_arg & 1), bool(c_arg & 2), self.names[c_arg >> 2])
10381034
else:
10391035
arg = self.names[c_arg]
1040-
elif c_instr.opcode in _opcode.hasfree:
1036+
elif c_instr.opcode in HAS_FREE:
10411037
if c_arg < ncells:
10421038
name = cells_lookup[c_arg]
10431039
arg = CellVar(name)
10441040
else:
10451041
name = self.freevars[c_arg - ncells]
10461042
arg = FreeVar(name)
1047-
elif c_instr.opcode in _opcode.hascompare:
1043+
elif c_instr.opcode in HAS_COMPARE:
10481044
arg = Compare(
10491045
(c_arg >> 4) if sys.version_info >= (3, 12) else c_arg
10501046
)
@@ -1120,7 +1116,6 @@ class _ConvertBytecodeToConcrete:
11201116
_compute_jumps_passes = 10
11211117

11221118
def __init__(self, code: _bytecode.Bytecode) -> None:
1123-
assert isinstance(code, _bytecode.Bytecode)
11241119
self.bytecode = code
11251120

11261121
# temporary variables
@@ -1180,8 +1175,8 @@ def concrete_instructions(self) -> None:
11801175
ConcreteInstr(
11811176
"CACHE", 0, location=self.instructions[-1].location
11821177
)
1183-
for i in range(self.required_caches)
11841178
]
1179+
* self.required_caches
11851180
)
11861181
self.required_caches = 0
11871182
self.seen_manual_cache = False
@@ -1201,7 +1196,6 @@ def concrete_instructions(self) -> None:
12011196

12021197
if isinstance(instr, TryBegin):
12031198
# We expect the stack depth to have be provided or computed earlier
1204-
assert instr.stack_depth is not UNSET
12051199
# NOTE here we store the index of the instruction at which the
12061200
# exception table entry starts. This is not the final value we want,
12071201
# we want the offset in the bytecode but that requires to compute
@@ -1235,43 +1229,28 @@ def concrete_instructions(self) -> None:
12351229
# fake value, real value is set in compute_jumps()
12361230
arg = 0
12371231
is_jump = True
1238-
elif instr.opcode in _opcode.hasconst:
1232+
elif instr.opcode in HAS_CONST:
12391233
arg = self.add_const(arg)
1240-
elif instr.opcode in _opcode.haslocal:
1234+
elif instr.opcode in HAS_LOCAL:
12411235
assert isinstance(arg, str)
12421236
arg = self.add(self.varnames, arg)
1243-
elif instr.opcode in _opcode.hasname:
1237+
elif instr.opcode in HAS_NAME:
12441238
if instr.name in BITFLAG_INSTRUCTIONS:
1245-
assert (
1246-
isinstance(arg, tuple)
1247-
and len(arg) == 2
1248-
and isinstance(arg[0], bool)
1249-
and isinstance(arg[1], str)
1250-
), arg
12511239
index = self.add(self.names, arg[1])
12521240
arg = int(arg[0]) + (index << 1)
12531241
elif instr.name in BITFLAG2_INSTRUCTIONS:
1254-
assert (
1255-
isinstance(arg, tuple)
1256-
and len(arg) == 3
1257-
and isinstance(arg[0], bool)
1258-
and isinstance(arg[1], bool)
1259-
and isinstance(arg[2], str)
1260-
), arg
12611242
index = self.add(self.names, arg[2])
12621243
arg = int(arg[0]) + 2 * int(arg[1]) + (index << 2)
12631244
else:
1264-
assert isinstance(arg, str), f"Got {arg}, expected a str"
12651245
arg = self.add(self.names, arg)
1266-
elif instr.opcode in _opcode.hasfree:
1246+
elif instr.opcode in HAS_FREE:
12671247
if isinstance(arg, CellVar):
12681248
cell_instrs.append(len(self.instructions))
12691249
arg = self.bytecode.cellvars.index(arg.name)
12701250
else:
1271-
assert isinstance(arg, FreeVar)
12721251
free_instrs.append(len(self.instructions))
12731252
arg = self.bytecode.freevars.index(arg.name)
1274-
elif instr.opcode in _opcode.hascompare:
1253+
elif instr.opcode in HAS_COMPARE:
12751254
if isinstance(arg, Compare):
12761255
# In Python 3.12 the 4 lowest bits are used for caching
12771256
# See compare_masks in compile.c
@@ -1284,7 +1263,6 @@ def concrete_instructions(self) -> None:
12841263
arg = arg.value
12851264

12861265
# The above should have performed all the necessary conversion
1287-
assert isinstance(arg, int)
12881266
c_instr = ConcreteInstr(instr.name, arg, location=instr.location)
12891267
if is_jump:
12901268
self.jumps.append((len(self.instructions), label, c_instr))

src/bytecode/instr.py

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
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
89
from typing import Any, Callable, Dict, Generic, Optional, Tuple, TypeVar, Union
910

@@ -21,23 +22,23 @@
2122
# Instructions relying on a bit to modify its behavior.
2223
# The lowest bit is used to encode custom behavior.
2324
BITFLAG_INSTRUCTIONS = (
24-
("LOAD_GLOBAL", "LOAD_ATTR")
25+
{"LOAD_GLOBAL", "LOAD_ATTR"}
2526
if sys.version_info >= (3, 12)
26-
else ("LOAD_GLOBAL",)
27+
else {"LOAD_GLOBAL"}
2728
if sys.version_info >= (3, 11)
28-
else ()
29+
else set()
2930
)
3031

31-
BITFLAG2_INSTRUCTIONS = ("LOAD_SUPER_ATTR",) if sys.version_info >= (3, 12) else ()
32+
BITFLAG2_INSTRUCTIONS = {"LOAD_SUPER_ATTR"} if sys.version_info >= (3, 12) else set()
3233

3334
# Intrinsic related opcodes
3435
INTRINSIC_1OP = (
35-
(_opcode.opmap["CALL_INTRINSIC_1"],) if sys.version_info >= (3, 12) else ()
36+
{_opcode.opmap["CALL_INTRINSIC_1"]} if sys.version_info >= (3, 12) else set()
3637
)
3738
INTRINSIC_2OP = (
38-
(_opcode.opmap["CALL_INTRINSIC_2"],) if sys.version_info >= (3, 12) else ()
39+
{_opcode.opmap["CALL_INTRINSIC_2"]} if sys.version_info >= (3, 12) else set()
3940
)
40-
INTRINSIC = INTRINSIC_1OP + INTRINSIC_2OP
41+
INTRINSIC = INTRINSIC_1OP | INTRINSIC_2OP
4142

4243

4344
# Used for COMPARE_OP opcode argument
@@ -242,25 +243,26 @@ class FreeVar(_Variable):
242243
def _check_arg_int(arg: Any, name: str) -> TypeGuard[int]:
243244
if not isinstance(arg, int):
244245
raise TypeError(
245-
"operation %s argument must be an int, "
246-
"got %s" % (name, type(arg).__name__)
246+
"operation %s argument must be an int, got %s" % (name, type(arg).__name__)
247247
)
248248

249249
if not (0 <= arg <= 2147483647):
250250
raise ValueError(
251-
"operation %s argument must be in " "the range 0..2,147,483,647" % name
251+
"operation %s argument must be in the range 0..2,147,483,647" % name
252252
)
253253

254254
return True
255255

256256

257257
if sys.version_info >= (3, 12):
258258

259+
@cache
259260
def opcode_has_argument(opcode: int) -> bool:
260261
return opcode in dis.hasarg
261262

262263
else:
263264

265+
@cache
264266
def opcode_has_argument(opcode: int) -> bool:
265267
return opcode >= dis.HAVE_ARGUMENT
266268

@@ -628,11 +630,9 @@ def stack_effect(self, jump: Optional[bool] = None) -> int:
628630
# 3.11 where LOAD_GLOBAL arg encode whether or we push a null
629631
# 3.12 does the same for LOAD_ATTR
630632
elif self.name in BITFLAG_INSTRUCTIONS and isinstance(self._arg, tuple):
631-
assert len(self._arg) == 2
632633
arg = self._arg[0]
633634
# 3.12 does a similar trick for LOAD_SUPER_ATTR
634635
elif self.name in BITFLAG2_INSTRUCTIONS and isinstance(self._arg, tuple):
635-
assert len(self._arg) == 3
636636
arg = self._arg[0]
637637
elif not isinstance(self._arg, int) or self._opcode in _opcode.hasconst:
638638
# Argument is either a non-integer or an integer constant,
@@ -845,13 +845,9 @@ def _check_arg(self, name: str, opcode: int, arg: InstrArg) -> None:
845845

846846
elif opcode in _opcode.hasconst:
847847
if isinstance(arg, Label):
848-
raise ValueError(
849-
"label argument cannot be used " "in %s operation" % name
850-
)
848+
raise ValueError("label argument cannot be used in %s operation" % name)
851849
if isinstance(arg, _bytecode.BasicBlock):
852-
raise ValueError(
853-
"block argument cannot be used " "in %s operation" % name
854-
)
850+
raise ValueError("block argument cannot be used in %s operation" % name)
855851

856852
elif opcode in _opcode.hascompare:
857853
if not isinstance(arg, Compare):

0 commit comments

Comments
 (0)