From c2a610ccd7fec8c4c8b70a33c7c545987a4a6944 Mon Sep 17 00:00:00 2001 From: "Gabriele N. Tornetta" Date: Wed, 27 May 2026 12:03:09 +0100 Subject: [PATCH] perf: cache InstrLocation across consecutive positions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) | --- src/bytecode/concrete.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/bytecode/concrete.py b/src/bytecode/concrete.py index 6f7f1c98..e99a7980 100644 --- a/src/bytecode/concrete.py +++ b/src/bytecode/concrete.py @@ -380,13 +380,20 @@ def from_code( pos_iter: Iterator[ Tuple[Optional[int], Optional[int], Optional[int], Optional[int]] ] = iter(code.co_positions()) + _last_pos: Optional[ + Tuple[Optional[int], Optional[int], Optional[int], Optional[int]] + ] = None + _last_loc: Optional[InstrLocation] = None for offset in range(0, len(bc), 2): op = bc[offset] arg = bc[offset + 1] if opcode_has_argument(op) else UNSET pos = next(pos_iter, None) - loc: Optional[InstrLocation] = ( - InstrLocation._from_tuple(*pos) if pos is not None else None - ) + if pos == _last_pos: + loc: Optional[InstrLocation] = _last_loc + else: + loc = InstrLocation._from_tuple(*pos) if pos is not None else None + _last_pos = pos + _last_loc = loc instructions.append(ConcreteInstr._from_opcode(opname[op], op, arg, loc)) bytecode = ConcreteBytecode()