Skip to content

Commit 25bf1bc

Browse files
perf: replace dis.get_instructions with direct co_code parsing in from_code (#194)
* perf: replace dis.get_instructions with direct co_code parsing in from_code dis.get_instructions performs two full passes over the bytecode: - _make_labels_map → findlabels → _unpack_opargs (to build a jump-label map) - _get_instructions_bytes (to iterate instructions with full metadata) Neither pass is needed here. ConcreteBytecode.from_code only needs the opname, raw arg byte, and source positions for each instruction word — all of which are directly available from co_code and co_positions(). CACHE entries are already inline in co_code on all supported Python versions, so direct 2-byte iteration handles them naturally without the per-version cache_info loop that 3.13 previously required. Throughput (round-trips of Bytecode.from_code().to_code() on the dis module's own code object, timed over 1 second, 3 runs each): Before: 92–94 round-trips/s After: 107–111 round-trips/s (~+17%) Austin CPU profile figures: dis._unpack_opargs: 5.98% own → eliminated dis._get_instructions_bytes: 3.45% own → eliminated ConcreteBytecode.from_code: 3.63% own → 4.91% own * Update src/bytecode/concrete.py Co-authored-by: Matthieu Dartiailh <marul@laposte.net> * assume co_positions always available * perf: bypass validation for trusted InstrLocation and Instr construction Add two fast-path factory methods that skip validation by using object.__new__ + direct slot assignment, for call sites where the inputs are already known to be valid: **InstrLocation._from_tuple** — replaces InstrLocation(...) at four internal sites where positions come from trusted sources (existing InstrLocation.lineno, SetLineno.lineno, first_lineno): - ConcreteBytecode.to_bytecode (fallback lineno-only location) - ConcreteBytecode._pack_location (propagated from existing location) - _ConvertBytecodeToConcrete.concrete_instructions (first_lineno seed and SetLineno-derived locations) **BaseInstr._from_trusted** — replaces Instr(name, arg, location=loc) in ConcreteBytecode.to_bytecode, where name/opcode/arg/location are all derived from already-validated ConcreteInstr objects. CPU own-time profile data: | Hotspot | Before | After | |---|---|---| | `ConcreteBytecode.to_bytecode` | 5.98% | 5.07% | | `Instr._check_arg` | 2.87% | eliminated | | `BaseInstr._set` (via to_bytecode) | 1.48% | eliminated | | `BaseInstr._from_trusted` | — | <1% (not in top 20) | Throughput (Bytecode.from_code().to_code() on dis module's code object, 1 second timed window, 5 runs): | | r/s range | |---|---| | Before | 103–108 | | After | 109–114 | * use faster _from_tuple * undo walrus --------- Co-authored-by: Matthieu Dartiailh <marul@laposte.net>
1 parent 9de3e78 commit 25bf1bc

1 file changed

Lines changed: 17 additions & 14 deletions

File tree

src/bytecode/concrete.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -336,21 +336,24 @@ def from_code(
336336
code: types.CodeType, *, extended_arg: bool = False
337337
) -> ConcreteBytecode:
338338
instructions: MutableSequence[Union[SetLineno, ConcreteInstr]] = []
339-
for i in dis.get_instructions(code, show_caches=True):
340-
loc = InstrLocation.from_positions(i.positions) if i.positions else None
341-
# dis.get_instructions automatically handle extended arg which
342-
# we do not want, so we fold back arguments to be between 0 and 255
343-
instructions.append(
344-
ConcreteInstr(
345-
i.opname,
346-
i.arg % 256 if i.arg is not None else UNSET,
347-
location=loc,
348-
)
339+
bc = code.co_code
340+
opname = _opcode.opname
341+
# co_positions() yields one (lineno, end_lineno, col_offset,
342+
# end_col_offset) per instruction word (including CACHE entries),
343+
# available from Python 3.11+. CACHE entries are already inline in
344+
# co_code on all supported versions, so iterating co_code directly
345+
# handles all versions without dis overhead.
346+
pos_iter: Iterator[
347+
Tuple[Optional[int], Optional[int], Optional[int], Optional[int]]
348+
] = iter(code.co_positions())
349+
for offset in range(0, len(bc), 2):
350+
op = bc[offset]
351+
arg = bc[offset + 1] if opcode_has_argument(op) else UNSET
352+
pos = next(pos_iter, None)
353+
loc: Optional[InstrLocation] = (
354+
InstrLocation._from_tuple(*pos) if pos is not None else None
349355
)
350-
# cache_info only exist on 3.13+
351-
for _, size, _ in (i.cache_info or ()) if PY313 else (): # type: ignore
352-
for _ in range(size):
353-
instructions.append(ConcreteInstr("CACHE", 0, location=loc))
356+
instructions.append(ConcreteInstr(opname[op], arg, location=loc))
354357

355358
bytecode = ConcreteBytecode()
356359

0 commit comments

Comments
 (0)