Python-level AST walks, the side boundary and the snapshot cache dominate large runs (2026-10-11)¶
Index: Known gaps
Status (2026-10-11 at b2e478513): OPEN. Measured on oneDAL (45/125 header roots), a 2021.x DWARF dump and a 28-member release; the cheap levers landed (see below), the structural ones did not.
Landed¶
materialize_locationsiterates siblings in a C-levelforloop and only touches its stack on descent (~15 % on a synthetic tree; the walk was 23.5 s self / 46 Misinstanceof 435 s profiled on oneDAL).- GC thresholds during a live dump:
storage.acyclic_json.gc_relaxed(#1544). ABICHECK_MEMORY_TRACEmarksdump.cache.lookup/dump.cache.store;ABICHECK_SNAPSHOT_CACHE_WRITE=0skips populating the snapshot cache.
Still open¶
- The clang JSON tree is walked at least eight times, each paying
Python per-node dispatch:
dict.getalone is 225 M calls (expr.walk 25.5 M,qualified_name_segments_walk._flag_fields24.5 M, templates.walk 15.4 M,_flag_plan14.4 M,dumper_clang._walk11.1 M,type_graph._node_fileandcall_graph._node_file7.8 M each). Fusing them (one visitor, per-pass callbacks) is the largest remaining CPU lever;json.loaditself is only 8 s of 435 s._node_fileis computed twice. - Side-boundary pickling: ~6 s
reduction.dumps+ ~2.5 s_pickle.loadsper run on the fork path;storage/json_compact._compactis the top abicheck self-frame there (4 %). GIL_MEMBER_PARALLELISM = 2: warm, a 28-member release ran j=1 in 29:45 / 3.99 GiB against j=2's 30:48 / 9.44 GiB (identical findings). The constant exists for the cold overlap of one member's clang children with another's Python; the cold j=1-vs-2 A/B has not been run. Lower it (or make it depend on a warm cache) only on that evidence.- On python3.15t a warm 28-member release is slower than cold
(9:57 vs 9:01, 33.7 vs 28.0 GiB, 2 925 vs 2 717 CPU s). Patching out the
cache population saved ~30 % wall / ~37 % RSS. Attribute with the new
dump.cache.*marks before changing the cache. Factconstruction:Fact.presentalready serves shared constants and enum members; the earlier "3.1x__post_init__" micro-win could not be located onFact(it has no__post_init__) — re-profile to find which owning dataclass it named.
Measured and rejected (oneDAL 2026.0 → 2026.1, libonedal.so.4 + oneapi/dal.hpp, 4 cores)¶
- Threads instead of forked children on a free-threaded build. python3.14t,
cold, whole-tree PSS sampled every 0.5 s: fork 99.1 s / 1.61 GiB, threads
112.4 s / 1.27 GiB. Threads trade ~13% wall for ~21% memory, so they are not
the
balanced(fastest) default; at most a memory-saving option. - Faster
json_compact._compact. A singlere.sub(rb"\n[ \t]*")pass over a real 445 MiB clang AST: 3.4-4.0 s vs 3.2-4.3 s for split/strip/join, identical output. Already at the pure-Python floor. - Pickle protocol 5 at the side boundary. A 50 MiB oneDAL snapshot:
~2.6 s
dumps/ ~1 sloads(gc paused) at protocol 4 or 5 alike. A snapshot loaded from storage costs far more (6-13 s) becausePendingGraph.__reduce__decodes its graph first, by contract; since #1544 a cache-served side is loaded in the parent, so that path is not pickled. - cProfile overstates per-node walks ~7x. Closure renumbering profiled at
~32 s/side but measures 4.4 s/side unprofiled (functions 2.0 s,
semantic_ir2.3 s over ~870k strings). It runs before dependency scoping; on the scoped snapshot the same walk is 0.35 s, but moving it changes closure ordinals, i.e. finding identities -- a maintainer decision. - Free-threaded warm slower than cold does not reproduce on one library
(3.14t: cold 124 s, warm 31.6 s;
ABICHECK_SNAPSHOT_CACHE_WRITE=0changes neither measurably); it is specific to the 28-member release fan-out.
Measurement method that worked¶
- Default-path cProfile only shows the parent blocked in
posix.read: profile the fork path with py-spy, attribute with cProfile underABICHECK_SIDE_PROCESSES=0. - RSS summed across forked children double-counts copy-on-write pages;
compare PSS (
/proc/<pid>/smaps_rollup) against a one-process run. tracemallocmultiplies wall time ~3x and RSS ~2x: memory only.- Peak RSS (
/usr/bin/time -v) plus a matchedABICHECK_MEMBER_JOBSA/B separates interpreter overhead from concurrency.