Skip to content

Commit c2a610c

Browse files
committed
perf: cache InstrLocation across consecutive positions
In ConcreteBytecode.from_code, InstrLocation._from_tuple is called once per instruction to construct a location object from the (lineno, end_lineno, col_offset, end_col_offset) tuple yielded by co_positions(). This was 5.31% of total CPU time in profiling. Python bytecodes for a single source expression (e.g. a + b, a for loop header, a CACHE entry) all map to the same source position. Because of this structural property, position tuples repeat frequently and tend to be contiguous — consecutive instructions usually share the same position. In the dis module corpus, 78.4% of the 860 position tuples are repeated. A single-entry cache (remember the last pos/loc pair) turns most iterations into a cheap tuple equality check, avoiding object.__new__ + 4x object.__setattr__ for the common case. A full dict cache was also tried but was slower: the per-iteration dict hash + lookup costs more than tuple equality even though it has a higher hit rate. Performance analysis (own time): | Hotspot | Before | After | |---|---|---| | InstrLocation._from_tuple own | 5.31% | 1.55% | Code object analysis (dis module) | Metric | Value | |---|---| | Total position tuples | 860 | | Unique positions | 318 (37%) | | Repeated positions | 542 (63%) | Throughput (Bytecode.from_code().to_code() on dis module, 30 runs each) with Mann-Whitney U Test | Approach | p95 | vs baseline | |---|---|---| | Baseline (main) | 180 r/s | — | | Dict cache | 190 r/s | +5.6% (✓ significant, p≈0) | | Single-entry cache | 192 r/s | +6.7% (✓ significant, p≈0) |
1 parent 41c679b commit c2a610c

1 file changed

Lines changed: 10 additions & 3 deletions

File tree

src/bytecode/concrete.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -380,13 +380,20 @@ def from_code(
380380
pos_iter: Iterator[
381381
Tuple[Optional[int], Optional[int], Optional[int], Optional[int]]
382382
] = iter(code.co_positions())
383+
_last_pos: Optional[
384+
Tuple[Optional[int], Optional[int], Optional[int], Optional[int]]
385+
] = None
386+
_last_loc: Optional[InstrLocation] = None
383387
for offset in range(0, len(bc), 2):
384388
op = bc[offset]
385389
arg = bc[offset + 1] if opcode_has_argument(op) else UNSET
386390
pos = next(pos_iter, None)
387-
loc: Optional[InstrLocation] = (
388-
InstrLocation._from_tuple(*pos) if pos is not None else None
389-
)
391+
if pos == _last_pos:
392+
loc: Optional[InstrLocation] = _last_loc
393+
else:
394+
loc = InstrLocation._from_tuple(*pos) if pos is not None else None
395+
_last_pos = pos
396+
_last_loc = loc
390397
instructions.append(ConcreteInstr._from_opcode(opname[op], op, arg, loc))
391398

392399
bytecode = ConcreteBytecode()

0 commit comments

Comments
 (0)