Skip to content

codegen: index constant dedup and cache nested-global lookup - #37

Draft
moreal wants to merge 4 commits into
mainfrom
perf/codegen-compile-time-quadratic
Draft

moreal wants to merge 4 commits into
mainfrom
perf/codegen-compile-time-quadratic

Conversation

@moreal

@moreal moreal commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Summary

Reduce repeated searches, recursive traversal, hashing, allocation and copying in RustPython's compiler, without changing Python semantics or bytecode.

Implementation details

  1. Narrow constant-dedup searches. Build on the original PR's digest indexes so insertion compares only relevant candidates rather than scanning every stored constant. Structural equality remains authoritative when hashes collide. Repair index lifecycle after clearing and compaction, preserving signed-zero, numeric-type, NaN and frozenset semantics.

  2. Avoid rehashing nested constants at every ancestor. Use shallow structural digests to narrow compiler code-constant comparisons, and exploit bottom-up canonicalization in the runtime constant cache so already-canonicalized child constants do not require repeated recursive hashing. The identity-based shortcut is private to the compiler cache and relies on canonicalized children; public code-object equality and hashing semantics are not replaced by pointer identity.

  3. Borrow analyzed symbol-table subtrees. Replace recursive subtree copies on scope activation with private active frames that borrow immutable analysis results. Keep replay and annotation cursors activation-local; copy symbol maps only when comprehensions temporarily override them. Collect nested-global names once instead of rewalking the tree for every module-level name, and avoid cloning skipped scopes.

  4. Reduce temporary allocation and copying. Move constants during compaction instead of deeply cloning them, borrow read-only constant operands, use amortized capacity growth, combine duplicate cache lookups, and avoid temporary tuples/integers used only to calculate a code object's hash. These changes remove redundant implementation work while retaining the same results and error behavior.

  5. Shrink private linear instruction storage. Separate CFG-only data and optional Python-export location snapshots from the native linear payload, and use bulk movement for instruction insertion. Entries shrink from 120 to 96 bytes on this arm64 build. Public instruction APIs, raw/semantic source locations, exception metadata and bytecode remain unchanged; the separately measured memory improvement below is not an additional general CPU-speedup claim.

Compile-only measurements

Measured on macOS arm64 with Rust 1.99.0 and CPython 3.15.1. Base is d1c3cb745af41fef0b1d273c24b4b7658daa3fa5; candidate compiler source is 0fc3ddab93b03ed530ed4318397b55631e200a13.

The latest comparison contains 16 inputs, three independent rounds with rotating interpreter order, one warmup and three retained samples per input/round: nine samples per input/interpreter. All inputs succeeded. Values below are median milliseconds; raw samples are retained rather than discarded.

Input CPython PR base Candidate Base/candidate speedup
Unique functions, 8,000 50.233 1395.018 45.115 30.92x
Folded lists, 8,000 40.277 759.233 24.421 31.09x
Nested lambdas, depth 120 0.425 9.840 0.553 17.78x
functools.py 3.935 4.810 4.261 1.13x
dataclasses.py 4.559 5.931 5.424 1.09x
typing.py 10.978 17.640 13.806 1.28x

Only compile(source, filename, "exec", flags=0, dont_inherit=True, optimize=0) is timed: parsing, symbol analysis, code generation and runtime code-object conversion. Startup, source generation, code execution and result destruction are excluded. Automatic cyclic GC is disabled during each case. No builds or tests run concurrently with timing.

These are single-machine observations, not universal speedup or asymptotic guarantees. The two RustPython builds use the same Rust toolchain/profile/features; CPython is a reference, not an identically configured build. The standard-library cases and nested lambdas still take longer than CPython. Earlier development experiments also covered all 46 default inputs, with repeated 45/75-sample comparisons where needed.

Real framework source corpus

The same compile-only workers also measured every .py file shipped under the framework's own package directory in official, SHA-256-verified pure-Python wheels. No framework or dependency was installed, imported or executed; transitive dependencies are not included.

The confirmation run used nine rounds, rotating interpreter order and reversing file order in alternating rounds, two warmups and five retained samples per file/round. All 1,063 files compiled successfully on all three interpreters, without dropped files or failed measurements. Base/candidate binaries and compiler flags are the same as above.

For each project and sample index within a round, sum the separately timed per-file compile durations, then take the median of those 45 sums. This is summed pure compilation time, not a contiguous package-import/startup or batch wall-time measurement. Timings are median ± sample standard deviation, in milliseconds:

Official source release Files CPython PR base Candidate Base/candidate speedup Compile-time reduction
Django 6.1.2 907 508.46 ± 13.58 579.46 ± 15.65 502.96 ± 8.07 1.15x 13.2%
Flask 3.1.3 24 21.24 ± 15.08 26.28 ± 1.58 22.71 ± 0.66 1.16x 13.6%
Sanic 25.12.1 132 70.99 ± 2.35 86.61 ± 6.98 75.98 ± 1.94 1.14x 12.3%

Every confirmation round's project-level median improved versus base. An initial nine-sample run also succeeded and is retained separately. CPython's Flask samples contain large outliers; none were discarded, and the high standard deviation limits a precise CPython-versus-candidate ranking. These results do not establish framework runtime compatibility, request throughput, startup latency, universal file-level improvements or statistical significance.

The corpus contains 5,918,998 source bytes for Django, 337,446 for Flask and 878,043 for Sanic. Wheel SHA-256 values are 141efee6ec64d1db6db90683bf734c550102450f444fb099063b0be1bd27d991, f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c and 3874f1fe0442e3b12ce4a3f6524f676d5d3c43839c6a4967d1b82fd8db53c0f3, respectively.

The wheel archives, manifest, exact per-file sources/hashes, one-off corpus adapter, initial/confirmation raw samples and aggregation results are retained locally under target/compile-bench/frameworks-20261027/. The adapter reuses the existing benchmark worker rather than adding framework-specific production code. Its package totals were independently recomputed from the raw per-round file results and checked against the saved summary.

Instruction-storage follow-up

On this arm64 build, private instruction-sequence entries shrink from 120 to 96 bytes, a 20% reduction per backing-array slot. The sequence header grows from 112 to 136 bytes; this is not a claim of 20% lower total compiler memory.

A separate depth-800 experiment used five fresh processes per interpreter. The control here is the ten-optimization snapshot immediately before the instruction-layout changes, not the PR base:

Whole-process metric, median MiB CPython Pre-layout control Final candidate Control/candidate reduction
Maximum RSS 27.61 55.83 52.30 6.3%
Peak memory footprint 20.09 40.44 36.73 9.2%

These values include interpreter startup and all compilation phases. They describe one workload on one machine, not compiler-phase allocations or universal savings. An intermediate 104-byte layout did not reduce process RSS and was refined further. Isolated 45/75-sample comparisons found no robust general compile-time speedup from the final layout changes; retain this as a storage/memory improvement, not an additional CPU speedup. Sampled profiles still show instruction allocation/initialization and memory movement as remaining costs.

Reproduction

Build base and candidate with the same release configuration in separate target directories, then run the controller under CPython:

cargo +1.99.0 build --release --locked --bin rustpython

python3.15 scripts/compile_bench.py \
  --cpython /absolute/path/to/python3.15 \
  --base /absolute/path/to/base/rustpython \
  --head /absolute/path/to/head/rustpython \
  --base-ref d1c3cb745af41fef0b1d273c24b4b7658daa3fa5 \
  --head-ref 0fc3ddab93b03ed530ed4318397b55631e200a13 \
  --workloads unique_defs,folded_lists,nested_lambdas,stdlib:functools,stdlib:dataclasses,stdlib:typing \
  --rounds 3 --samples 3 --warmups 1 --output target/compile-bench/comparison

Use actual executables rather than version-manager shims, keep the machine idle, and choose a fresh output directory. Each run writes report.md, raw/per-round results.json, and exact inputs in sources.json, including executable/source hashes and the reproduction command. Failures remain errors and cannot appear as speedups. Revision labels are caller-supplied; RustPython can retain an older embedded Git version string, so that string alone is not provenance.

Measured executable SHA-256 values:

  • Base: 548afe03bf05879c4473e977fb978c87172f6cd6bdde906e8bcaa4f67d1c5622
  • Candidate: 78d238b987428d3f1231f5f8b1c13ab84847006d2939b0d3470e829acdc2ab82
  • CPython: cd353074fb2780e9ed5bdaf2ed6aeab5bd9fc0c883041ea2228da4b653d97903
  • Pre-layout memory control: 14a114077bf5e526a6da4ff67afd06e20a5527e61fbe9a8d397753e16436799e

The latest raw comparison is retained locally as target/compile-bench/pr37-pr-summary-20261027-0fc3ddab9/; earlier experiment data are also retained locally, not committed as generated artifacts.

Correctness and local validation

Regression coverage includes constant-index lifecycle/compaction, digest collisions, signed zero, numeric type distinctions, NaN-bearing constants, frozenset order independence, canonical constant-cache hashing, skipped/nested scopes, finally replay, annotations, comprehensions, and instruction-location/label/handler preservation. Hashes only narrow candidates; equality still decides deduplication. Compiler-private canonical hashing does not replace public code-object hash semantics.

  • Workspace tests excluding wasm, venvlauncher and C-API: 1,477 passed, 20 ignored, zero failed.
  • Separate C-API tests: 128 passed, four ignored, zero failed.
  • Workspace and separate C-API Clippy with --all-targets -- -D warnings: passed.
  • Release Python tests: all nine selected modules passed (test_code, test_compile, test_symtable, test_scope, test_marshal, test_listcomps, test_setcomps, test_dictcomps, test_genexps; reported run=460, skipped=85).
  • Benchmark measurement contracts passed under both RustPython and CPython; formatting/lint hooks and diff whitespace checks passed. These are local results, not a statement that remote CI has completed.

Related prior work: the older perf/compile-time branch. This PR is written against its stated base rather than rebasing that branch.

AI assistance

The original implementation disclosed choco-pi:deepseek-v4.1-flash-1017 in its commit. Subsequent design, implementation, regression tests, performance experiments and review were assisted by Codex (gpt-6.1-sol), with disclosure in the corresponding commit messages. This does not substitute for human review.

Compile time grew superlinearly: 8000 top-level defs cost 3375 ms
against CPython's 57 ms, because every constant insert scanned the
whole pool with a deep structural compare and every module-level
name re-walked the symbol-table tree.

Assisted-by: choco-pi:deepseek-v4.1-flash-1017
Signed-off-by: Lee Dogeon <[email protected]>
@github-actions

Copy link
Copy Markdown

📦 Library Dependencies

The following Lib/ modules were modified. Here are their dependencies:

[ ] lib: cpython/Lib/profiling
[ ] test: cpython/Lib/test/test_profiling

dependencies:

  • profiling (native: main, _child_monitor, _css_utils, _format_utils, _lsprof, _remote_debugging, binary_collector, binary_reader, cli, collector, constants, display, dump, errors, gecko_collector, heatmap_collector, importlib.machinery, importlib.resources, importlib.util, itertools, jsonl_collector, live_collector, marshal, math, module_utils, opcode_utils, profiling.tracing, profiling.tracing._utils, pstats_collector, sample, stack_collector, string_table, sys, time, trend_tracker, widgets)
    • curses (native: _curses, _curses_panel, curses.ascii, has_key, sys)
    • io (native: _io, _thread, errno, msvcrt, sys)
    • opcode (native: _opcode, builtins)
    • platform (native: _wmi, itertools, sys, vms_lib, winreg)
    • site (native: _io, _pyrepl.main, _pyrepl.pager, _pyrepl.readline, _pyrepl.unix_console, _pyrepl.windows_console, atexit, builtins, errno, readline, sitecustomize, sys, usercustomize)
    • sysconfig (native: _sysconfig, _winapi, importlib.machinery, importlib.util, os.path, sys)
    • _colorize, abc, argparse, base64, collections, contextlib, dataclasses, functools, html, json, linecache, locale, optparse, os, pathlib, pstats, re, runpy, selectors, socket, statistics, subprocess, tempfile, threading, traceback, types, typing, uuid, webbrowser

dependent tests: (3 tests)

  • profiling: test_profiling
    • cProfile: test_cprofile test_pstats

[ ] test: cpython/Lib/test/test_set.py (TODO: 1)

dependencies:

dependent tests: (no tests depend on set)

[x] lib: cpython/Lib/traceback.py
[x] test: cpython/Lib/test/test_traceback.py (TODO: 2)

dependencies:

  • traceback

dependent tests: (170 tests)

  • traceback: test_asyncio test_builtin test_code_module test_concurrent_futures test_contextlib test_contextlib_async test_coroutines test_dictcomps test_exceptions test_http_cookiejar test_importlib test_iter test_listcomps test_pyexpat test_setcomps test_socket test_ssl test_subprocess test_sys test_threadedtempfile test_threading test_traceback test_unittest test_with test_zipimport
    • code:
      • pdb: test_pdb
      • sqlite3.main: test_sqlite3
    • concurrent.futures.interpreter: test_concurrent_futures
    • concurrent.futures.process: test_compileall test_concurrent_futures
    • http.cookiejar: test_urllib2
      • urllib.request: test_httpservers test_pathlib test_pydoc test_sax test_site test_urllib test_urllib2_localnet test_urllib2net test_urllibnet
    • logging: test_asyncio test_decimal test_genericalias test_hashlib test_logging test_pkgutil test_support test_unittest
      • multiprocessing.util: test_asyncio test_concurrent_futures
      • venv: test_venv
    • multiprocessing: test_fcntl test_importlib test_memoryview test_multiprocessing_forkserver test_multiprocessing_main_handling test_profiling test_re
    • profiling.sampling.dump: test_profiling
    • py_compile: test_cmd_line_script test_importlib test_modulefinder test_py_compile test_runpy
      • zipfile: test_shutil test_zipapp test_zipfile test_zipfile64 test_zipimport_support
    • pydoc: test_enum
      • xmlrpc.server: test_docxmlrpc test_xmlrpc
    • socketserver: test_imaplib test_socketserver test_wsgiref
    • threading: test_android test_asyncio test_bytes test_bz2 test_code test_concurrent_futures test_context test_ctypes test_email test_enumerate test_external_inspection test_fork1 test_frame test_ftplib test_functools test_gc test_httplib test_importlib test_inspect test_io test_ioctl test_itertools test_linecache test_opcache test_pathlib test_poll test_poplib test_pyrepl test_queue test_robotparser test_sched test_signal test_smtplib test_sqlite3 test_super test_syslog test_termios test_threading_local test_time test_weakref test_winreg test_zstd
      • bdb: test_bdb
      • dummy_threading: test_dummy_threading
      • importlib.util: test_asdl_parser test_ctypes test_doctest test_importlib test_reprlib
      • queue: test_dummy_thread
      • subprocess: test_asyncio test_atexit test_audit test_c_locale_coercion test_cmd_line test_ctypes test_dtrace test_embed test_faulthandler test_file_eintr test_gzip test_json test_launcher test_msvcrt test_ntpath test_os test_osx_env test_peg_generator test_platform test_plistlib test_profiling test_pyrepl test_quopri test_regrtest test_repl test_script_helper test_select test_sys_settrace test_sysconfig test_tempfile test_tkinter test_tstring test_unittest test_utf8_mode test_wait3 test_webbrowser test_xpickle
      • sysconfig: test_importlib test_os test_tools
      • trace: test_trace
    • timeit: test_timeit
    • tkinter: test_tkinter test_ttk test_ttk_textonly

[x] test: cpython/Lib/test/test_external_inspection.py

dependencies:

dependent tests: (no tests depend on external_inspection)

Legend:

  • [+] path exists in CPython
  • [x] up-to-date, [ ] outdated

Keep constant indexes coherent across clear and compaction, preserve frozenset and NaN semantics, and narrow code-object comparisons with collision-safe digests. Avoid recursive subtree hashing and repeated inner-scope scans, and borrow comprehension metadata without cloning a scope tree. Add regression coverage for index lifecycle, digest collisions, numeric keys, and scope summaries.

Assisted-by: Codex:gpt-6.1-sol
@moreal moreal added the run:pyperformance Run the base/head/CPython pyperformance comparison job label Oct 26, 2026 — with ChatGPT Codex Connector
@codspeed

codspeed Bot commented Oct 26, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 62 untouched benchmarks
⏩ 4 skipped benchmarks1


Comparing perf/codegen-compile-time-quadratic (3d0bf54) with main (d1c3cb7)

Open in CodSpeed

Footnotes

  1. 4 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

<!-​- pyperformance-local-diff -->

pyperformance: base vs. head vs. CPython (same runner)

All three interpreters were measured back to back in this one job, so they share the exact same CPU and kernel. Ratios are time / CPython 3.15.0 time -- lower is better. A head/base difference smaller than 3% is shown as ~; CI runners are not quiet enough to read more into it than that.

base head
Median slowdown vs. CPython 3.15.0 (27 common) 5.18x 5.21x
Benchmarks passed 27 27
Geometric mean, head/base (27 common) 1.000
Benchmark CPython 3.15.0 base head base/CPython 3.15.0 head/CPython 3.15.0 head vs. base
mdp 1.43 sec +- 0.03 sec (timeout) (timeout) - - -
unpack_sequence 38.7 ns +- 0.2 ns 151 ns +- 2 ns 141 ns +- 1 ns 3.90x 3.64x -6.6%
unpickle_pure_python 202 us +- 1 us 1.51 ms +- 0.02 ms 1.48 ms +- 0.01 ms 7.48x 7.33x ~
go 104 ms +- 1 ms 725 ms +- 3 ms 716 ms +- 5 ms 6.97x 6.88x ~
json_dumps 9.63 ms +- 0.04 ms 30.4 ms +- 0.2 ms 30.1 ms +- 0.2 ms 3.16x 3.13x ~
json_loads 24.5 us +- 0.2 us 55.1 us +- 0.3 us 54.6 us +- 0.2 us 2.25x 2.23x ~
deepcopy 266 us +- 3 us 1.31 ms +- 0.00 ms 1.30 ms +- 0.01 ms 4.92x 4.89x ~
generators 30.0 ms +- 0.5 ms 329 ms +- 3 ms 328 ms +- 1 ms 10.97x 10.93x ~
comprehensions 16.4 us +- 0.1 us 124 us +- 1 us 124 us +- 2 us 7.56x 7.56x ~
deltablue 2.95 ms +- 0.02 ms 24.8 ms +- 0.2 ms 24.8 ms +- 0.1 ms 8.41x 8.41x ~
float 87.0 ms +- 1.2 ms 326 ms +- 1 ms 326 ms +- 3 ms 3.75x 3.75x ~
logging 6.18 us +- 0.07 us 32.0 us +- 0.1 us 32.0 us +- 0.2 us 5.18x 5.18x ~
richards 41.8 ms +- 0.7 ms 248 ms +- 2 ms 248 ms +- 2 ms 5.93x 5.93x ~
telco 160 ms +- 1 ms 756 ms +- 3 ms 757 ms +- 4 ms 4.72x 4.73x ~
regex_v8 21.3 ms +- 0.3 ms 65.7 ms +- 1.2 ms 65.8 ms +- 1.4 ms 3.08x 3.09x ~
hexiom 5.62 ms +- 0.03 ms 37.8 ms +- 0.1 ms 37.9 ms +- 0.2 ms 6.73x 6.74x ~
xml_etree 148 ms +- 3 ms 978 ms +- 20 ms 981 ms +- 21 ms 6.61x 6.63x ~
regex_compile 133 ms +- 1 ms 597 ms +- 2 ms 599 ms +- 3 ms 4.49x 4.50x ~
coroutines 24.6 ms +- 0.3 ms 232 ms +- 1 ms 233 ms +- 1 ms 9.43x 9.47x ~
nqueens 91.2 ms +- 1.5 ms 534 ms +- 3 ms 538 ms +- 4 ms 5.86x 5.90x ~
scimark 354 ms +- 1 ms 940 ms +- 3 ms 948 ms +- 10 ms 2.66x 2.68x ~
typing_runtime_protocols 198 us +- 5 us 1.05 ms +- 0.01 ms 1.06 ms +- 0.00 ms 5.30x 5.35x ~
raytrace 298 ms +- 1 ms 1.94 sec +- 0.01 sec 1.96 sec +- 0.01 sec 6.51x 6.58x ~
spectral_norm 119 ms +- 2 ms 475 ms +- 2 ms 481 ms +- 2 ms 3.99x 4.04x ~
chaos 62.8 ms +- 0.3 ms 322 ms +- 2 ms 327 ms +- 1 ms 5.13x 5.21x ~
pickle_pure_python 326 us +- 2 us 1.89 ms +- 0.01 ms 1.92 ms +- 0.01 ms 5.80x 5.89x ~
fannkuch 438 ms +- 2 ms 1.74 sec +- 0.01 sec 1.77 sec +- 0.01 sec 3.97x 4.04x ~
nbody 119 ms +- 1 ms 432 ms +- 2 ms 441 ms +- 11 ms 3.63x 3.71x ~

Borrow analyzed symbol-table subtrees through lightweight active frames, avoiding recursive copies while preserving replay and annotation state. Reduce duplicate constant hashing, cache lookups, temporary runtime allocations, and compaction copies with collision-safe compiler keys.

Separate compact linear instruction payloads and optional Python export locations from CFG-only metadata. Preserve public instruction APIs, source positions, exception handlers, and bytecode semantics.

Add correctness and storage regressions plus a reproducible CPython/base/head compile-only benchmark, contract tests, and experimental results. Validate workspace and C-API tests, relevant Python modules, both Clippy configurations, and repository hooks.

Assisted-by: Codex:gpt-6.1-sol
Move essential usage and measurement caveats into the benchmark module docstring. Remove the repository experiment log after moving its summary and reproduction details into the PR description.

Keep eight core measurement-contract tests while removing auxiliary workload checks. Leave compiler regressions and benchmark timing logic unchanged.

Assisted-by: Codex:gpt-6.1-sol
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run:pyperformance Run the base/head/CPython pyperformance comparison job z-ca-2026

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant