Performance Optimizations¶
This document summarises the performance optimizations applied to uconf,
both historical and current. Measurements reference the benchmark in
benchmark.py: computing compute_chain_complex for the euclidean
unordered configuration model on R² over GF(2) at weight 3.
Existing optimizations¶
1. RootedTree with eagerly cached structural properties¶
Files: src/uconf/core/trees.py
Replaced the legacy nested-tuple tree representation with an immutable,
hashable RootedTree class that computes weight, leaves, min_leaf,
tree_arity, and hash at construction time. All structural queries are
now O(1) instead of O(tree_size).
This eliminates repeated O(v) traversals in hot paths such as
to_shuffle_tree_bar/cobar, validate_tree, and _d2_on_basis.
See tree-redesign.md for the full design rationale.
2. Planar coinvariant enumeration (~2.8× speedup)¶
Files: src/uconf/algebraic/cofree_coalgebra.py,
src/uconf/algebraic/tree_module.py
Instead of explicitly constructing full S_n orbit sums for the cofree
coalgebra basis, enumerate planar representatives directly and canonicalize
non-planar boundary output back to planar keys. Uses the isomorphism
(P_pl ⊗ k[S_n]) ⊗_{S_n} M^{⊗n} ≅ P_pl ⊗ M^{⊗n}.
Observed speedup: ~2.8× (226s → 80s on heavy layers). Validated
d² = 0 over QQ and GF(2).
See tree-redesign.md.
3. Connectivity short-circuit in compute_chain_complex¶
File: src/uconf/homology.py
If a degree is below the module’s connectivity, the basis is trivially empty. Skips basis enumeration and boundary matrix assembly entirely for such degrees.
4. Sparse matrix assembly¶
File: src/uconf/homology.py
_boundary_matrix uses SageMath’s sparse matrix format by default, which
is faster and more memory-efficient for the typically sparse differentials.
5. @cached_method on boundary/differential methods¶
Files: bar_construction.py, cobar_construction.py,
bar_algebra.py, cobar_coalgebra.py, tree_module.py,
cofree_coalgebra.py
The methods _boundary_on_basis, _d1_on_basis, _d2_on_basis,
_dalpha_on_basis, and _twisted_boundary_on_basis are all cached
via SageMath’s @cached_method. Since trees are immutable and hashable,
cache lookups are efficient and results are never recomputed.
6. Pre-cached permutation lists¶
File: src/uconf/morphisms/e_comodule_morphism.py
_non_identity_perms(n) is decorated with @cached_function,
materialising the non-identity elements of S_n exactly once per arity for
the lifetime of the process. This avoids repeated GAP bridge calls.
7. Validated-tree fast paths (_from_validated_tree)¶
Files: bar_construction.py, cobar_construction.py
Internal boundary computations produce trees from known-valid inputs.
_from_validated_tree skips _validate_basis_key (and its recursive
validate_tree traversal) while still normalising to shuffle form.
Optimizations applied 2026-04-08¶
Benchmark baseline¶
Metric |
Before |
After |
|---|---|---|
Wall-clock time |
29.14 s |
11.97 s |
Speedup |
— |
2.43× |
|
132,567 |
5,829 |
|
9.45 s |
0.70 s |
|
113,556 |
78,269 |
|
3.88 s |
2.83 s |
|
187,888 |
150,817 |
|
13.57 s |
3.75 s |
Total function calls |
30.5 M |
18.3 M |
8. Batched d_sigma_decompose (dominant optimisation)¶
Files: src/uconf/core/quasi_planar.py,
src/uconf/morphisms/e_comodule_morphism.py
Problem. The recursive E-comodule computation (_nu_on_planar.recurse)
called d_sigma(x, σ) once per non-identity permutation σ ∈ S_n. Each
call independently computed boundary(x) and planarize(∂x), then
scanned the result for the single σ-component. For arity n = 4;
|S_4| − 1 = 23 calls per element, totalling 132 K calls and 9.45 s.
Fix. Added d_sigma_decompose(x) to QuasiPlanarMixin, which
computes boundary + planarize once and returns a dictionary
{σ: d_σ(x)} of all non-zero components. Modified recurse to call
d_sigma_decompose once per element and iterate over the (sparse) result.
Impact. 132 K → 5.8 K calls (22.7× reduction). Cumulative time 9.45 s → 0.70 s (13.5× speedup on this path). Overall benchmark improvement of roughly 10 s.
9. Memoized subtree_degree / subtree_degree_cobar¶
File: src/uconf/core/trees.py
Both functions recursively compute the total shifted bar/cobar degree of
a subtree. Since RootedTree instances are immutable and hashable, the
results are now cached via @cached_function keyed on
(tree, operad_cls, base_ring). Called during shuffle tree normalisation
(to_shuffle_tree_bar/cobar) and sign computation.
10. Bypass validation in _extend_tree¶
File: src/uconf/morphisms/e_comodule_morphism.py
In _extend_tree, cobar elements are constructed from known-valid
cooperad keys (produced by e_comodule_on_generator). Changed
cobar_k(cobar_tree) to cobar_k._from_validated_tree(cobar_tree) and
target_k((be_key, cobar_key)) to target_k.term((be_key, cobar_key)),
skipping redundant validation and shuffle normalisation for single-vertex
corollas.
Impact. Reduced validate_tree calls by ~31 % (113 K → 78 K) and
hadamard_operad._validate_basis_key calls by ~34 %.
Optimizations applied 2026-04-09¶
Benchmark baseline (weight 3)¶
Metric |
Before |
After |
|---|---|---|
Wall-clock time |
~10 s |
~4.3 s |
Speedup |
— |
~2.3× |
Total function calls |
~18.3 M |
~8.6 M |
11. Cached table reduction per basis key¶
File: src/uconf/models/__init__.py
Introduced _compute_table_reduction_cached decorated with
@cached_function keyed on (arity, base_ring, basis_key).
Previously table reduction was recomputed each time a BE basis element
appeared in a different context; this eliminates ~8× redundant
recomputation.
12. Bypass Sage morphism.__call__ overhead¶
Files: src/uconf/homology.py, src/uconf/algebraic/free_algebra.py,
src/uconf/algebraic/cofree_coalgebra.py,
src/uconf/algebraic/hadamard_algebra.py,
src/uconf/core/quasi_planar.py
Each morphism(term(key)) call adds ~2 µs overhead from
linear_combination / generator dispatch. For 90 K+ calls this
dominates.
Extracted get_on_basis() helper in core/signs.py to uniformly access
the underlying on_basis callable. Applied in:
_boundary_matrix: callon_basis(key)directly instead ofboundary(elem)_normalized_corolla_sum: call_planarize_on_basis(key)directly instead ofplanarize(term(key))d_sigma_decompose: bypass both boundary and planarize wrappersfree_algebra._boundary_on_basis: call inner-module boundary directly
13. Dict accumulation over element construction¶
Files: src/uconf/algebraic/hadamard_algebra.py,
src/uconf/morphisms/e_comodule_morphism.py,
src/uconf/algebraic/configuration.py,
src/uconf/algebraic/free_algebra.py,
src/uconf/algebraic/cofree_coalgebra.py
Replaced the pattern result += coeff * module.term(key) (which creates
an intermediate Sage element per term) with {key: coeff} dict
accumulation followed by a single sum_of_terms() call. Eliminates
thousands of intermediate Sage element constructions per boundary
evaluation.
14. Cache morphism result in PullbackAlgebra.act¶
File: src/uconf/algebraic/pullback_algebra.py
The pullback algebra action γ^Q(f(p); a_1, …, a_n) calls
morphism(p_element) for each p. The morphism result depends only
on p, not on the algebra inputs a_i. Added a per-instance dict
cache keyed on tuple(p_element) to avoid redundant morphism
evaluations.
15. Bypass validation for known-valid keys¶
Files: src/uconf/constructions/cobar_construction.py,
src/uconf/algebraic/hadamard_algebra.py,
src/uconf/models/surjection.py
Use
_from_validated_treein cobarcomposefor internally-grafted trees.Use
self.module.term(key)instead ofself.module(key)inhadamard_algebra._act_implfor keys that are already validated.Optimise
surjection._validate_basis_keyto a single-pass combined bounds + degeneracy check.
16. Direct _compute_table_reduction_cached call¶
File: src/uconf/algebraic/configuration.py
configuration._on_element now calls the @cached_function directly
instead of constructing a BE element and invoking
element.table_reduction(), bypassing element construction and morphism
dispatch.
Remaining bottlenecks (for future work)¶
After the 2026-04-09 optimisations, the top cumulative-time functions are (weight-3 benchmark, first run):
Function |
Cumtime |
Notes |
|---|---|---|
|
3.6 s |
45 % — recursive cooperad coaction |
|
2.6 s |
Cartesian-product term expansion |
|
1.8 s |
Operadic algebra action |
|
0.85 s |
Partition enumeration (one-time) |
|
1.3 s |
83 K calls — Sage element construction |
|
8.0 s |
Full boundary loop over 650 basis elements |
Evaluated optimisation leads¶
The following were listed as potential future optimisations (2026-04-08) and have been evaluated against the current profile:
Cache
Lie._permute_on_basis: ✅ Already implemented (has@cached_methodsince the 2026-04-08 round).GF(2) sign short-circuit: ❌ Not worthwhile. Profiling shows
sign_from_exponentandkoszul_sign_of_permutationtogether total only ~30 ms (0.04 s), less than 1 % of wall time. The refactoring cost (threading base-ring characteristic through every call site) outweighs the benefit.Skip validation in more internal paths: ✅ Partially done (see §15). Remaining
validate_tree/validate_vertexcalls (~84 K, 0.47 s) come from_from_validated_tree→_normalize_to_shuffle→ shuffle tree normalisation, which still validates vertex ordering. These are structurally necessary.Batch boundary + planarize in
_planarize_on_basis: ✅ Already achieved viad_sigma_decompose(§8) and recursive_planarize_subtree.
New leads¶
E-comodule morphism caching or algebraic reformulation: The recursive
_nu_on_planar.recursefunction (1.1 s, 9.7 K calls) dominates the pipeline. Caching intermediate results keyed on(current_d_elem, len(sigma_bar))could help if many branches converge to the same intermediate element. Alternatively, an algebraic reformulation that avoids the recursion entirely (e.g. computing the acyclic-models map via iterated tensor products) could yield larger gains.Reduce
sum_of_termsoverhead: With 83 K calls at 1.3 s, the SageCombinatorialFreeModule.sum_of_termsmethod is a significant overhead. For internal constructions that produce validated keys, directly building the_from_dictrepresentation could bypass thesum_of_terms→_from_dict→__classcall_private__chain.Table reduction algorithm: The current
term_generator(0.8 s) enumerates all permutations of integer partitions viaset(permutations(pi_ord)). For larger arities, a direct algorithm that avoids redundant partition permutations could reduce this one-time cost.Parallel boundary matrix assembly: Since boundary evaluations for different basis elements are independent, the
_boundary_matrixloop could be parallelised across multiple processes, though Sage’s GIL constraints limit the benefit of threading.
Optimizations applied 2026-04-27¶
17. Direct on_basis path in _boundary_matrix¶
File: src/uconf/homology.py
compute_chain_complex used to assemble each differential column by calling
module.boundary(elem) on a one-term basis element. Even when the module
exposed an on_basis callback, Sage still paid the full
morphism.__call__ -> linear_combination overhead for every column.
_boundary_matrix now detects the underlying on_basis function with
get_on_basis() and calls it directly on the source key. This preserves the
existing fallback path for generic modules, while avoiding thousands of tiny
single-term morphism evaluations during chain-complex assembly.
To keep compatibility with modules that normalize keys only at construction
time, _boundary_matrix still falls back to module._normalize_key(...)
when a returned key is not already present in the target basis.
Benchmark impact¶
On the weight-3 Euclidean configuration benchmark (GF(2), dim=2):
Metric |
Before |
After |
|---|---|---|
|
7.79 s |
4.29 s |
|
15.80 s |
6.79 s |
After this change, the dominant remaining cost is no longer chain-complex
assembly overhead but the recursive Le Grignou-Roca i Lucio E-comodule map
(src/uconf/morphisms/e_comodule_morphism.py) together with the d_alpha
twisting-differential pipeline.
18. Dynamic-programming fast paths in the Le Grignou–Roca i Lucio comodule map¶
File: src/uconf/morphisms/e_comodule_morphism.py
The recursive cooperad-level map still spent most of its time in two kinds of repeated work:
recomputing the cumulative permutation product
sigma_k * ... * sigma_1from scratch at every recursion depth, andrebuilding the same generator/subtree images many times while traversing cobar trees with repeated decorations.
This was addressed in three places:
_nu_on_planar.recursenow threads the cumulative permutation product through the recursion instead of recomputing it fromsigma_bar.The equivariant cooperad action uses direct
_permute_on_basisfast paths when available, avoiding repeated element-wrapperpermute()overhead.make_e_comodule_morphismmemoizes both generator root images and full subtree images, turning the tree extension step into a small dynamic program over repeated decorations/subtrees.
Benchmark impact¶
On the arity-3 comodule sub-benchmark in benchmark_detailed.py, the
Le Grignou–Roca i Lucio map dropped from about 6.2 s before this round
to about 1–3 s after it, depending on cache warmth and process startup.
In an isolated cProfile run over all arity-3 basis elements:
Metric |
Before |
After |
|---|---|---|
Total time |
5.05 s |
2.43 s |
|
4.59 s |
2.18 s |
|
3.76 s |
1.76 s |
|
1.40 s |
0.72 s |
Optimizations applied 2026-04-30¶
19. Basis-level action caches in free/Hadamard algebras¶
Files: src/uconf/algebraic/free_algebra.py,
src/uconf/algebraic/hadamard_algebra.py
The bar differential repeatedly applies algebra actions to the same basis
inputs with different scalar coefficients. Previously both
FreeOperadAlgebra._act_impl and HadamardTensorAlgebra._act_impl
recomputed the full substitution / graded-interchange / normalization pipeline
for every call, even when the underlying basis tuple had already appeared in an
earlier boundary term.
This round adds cached basis-level helpers:
FreeOperadAlgebra._act_on_basis_tuple(q_key, input_keys)HadamardTensorAlgebra._act_on_basis_tuple(had_basis, tensor_basis_tuple)
The public _act_impl methods now only expand coefficients and reuse the
cached basis result. This pushes repeated operadic substitution, normalization,
and tensor-product output construction behind @cached_method.
20. Cached bar/E-comodule intermediates and cheaper normalization¶
Files: src/uconf/constructions/bar_algebra.py,
src/uconf/morphisms/e_comodule_morphism.py,
src/uconf/algebraic/cofree_coalgebra.py,
src/uconf/homology.py
Several remaining hotspots were still rebuilding the same intermediate objects:
BarAlgebraModule._dalpha_*now cachesα(c_R)on basis keys and the normalized left-corolla outputs(c_L, new_m)._nu_on_planarnow memoizes repeatedd_sigma_decomposecalls, repeated cooperad-factor permutations, and repeatedrho(sigma_bar)evaluations inside one recursion tree.CofreeCoalgebraModule._normalized_corolla_sumand_boundary_on_basisnow accumulate directly into dictionaries instead of repeatedly doingresult += ...._boundary_matrixnow caches normalized target-row matches for repeated raw boundary keys, reducing duplicate_normalize_key(...)work during matrix assembly.
21. Cold vs warm benchmark split¶
Files: benchmark.py, benchmark_detailed.py
The benchmark scripts now run cold and warm passes separately in the same process so that one-time cache fill can be compared against steady-state costs.
On the weight-3 Euclidean configuration benchmark (GF(2), dim=2):
Metric |
Cold |
Warm |
|---|---|---|
|
19.58 s |
0.056 s |
|
5.22 s |
1.67 s |
|
0.407 s |
~0.000 s |
|
20.97 s |
18.98 s |
The serial detailed benchmark now makes the cache split explicit:
One-time setup + cache fill: ~19.60 s
Steady-state full chain complex: ~0.056 s
The parallel profiled benchmark remains much slower on the warm pass because
forked workers do not reuse the main process’s cached @cached_method state;
the wall time is now dominated by process/lock overhead, while the merged
worker profiles still identify the same mathematical hot paths.
Current bottleneck split after this round¶
From benchmark_profile.txt (merged worker profiles; cumulative times sum
across workers and therefore exceed wall clock):
bar_algebra._dalpha_all_splits: 118.9 s cumulativeconfiguration._on_element: 88.4 se_comodule_on_generator: 70.7 se_comodule._on_element: 74.2 shadamard_algebra._act_impl: 28.6 shadamard_algebra._act_on_basis_tuple: 28.0 sfree_algebra._act_impl: 24.4 sfree_algebra._act_on_basis_tuple: 23.1 squasi_planar.d_sigma_decompose: 18.8 scofree_coalgebra._boundary_on_basis: 13.0 scofree_coalgebra._normalized_corolla_sum: 5.3 s
So the next work should stay focused on the d_alpha / comodule / algebra
action stack, not on table reduction or basis enumeration.
Optimizations applied 2026-05-06¶
Benchmark baseline (weight 3, deg_max 5, serial)¶
Metric |
Before |
After |
|---|---|---|
Wall-clock time |
27.98 s |
11.52 s |
Speedup |
— |
2.43× |
|
16.50 s |
0.17 s |
|
16.64 s |
0.49 s |
|
16.78 s |
0.61 s |
22. Pure-Python _compute_table_reduction_cached rewrite¶
File: src/uconf/models/__init__.py
Problem. The term_generator inner loop inside _compute_table_reduction_cached
used Partitions(d+n, length=d+1) + set(permutations(pi_ord)) to enumerate
all ordered compositions of d+n into d+1 positive parts. For a partition
like [3,1,1,1,1] this generated 720 permutation candidates and deduped to 5
unique tuples; the set/permutation overhead accumulated to 16.5 s tottime
(60 % of wall time) across 3 666 cache misses.
Three issues were fixed:
Composition enumeration via
itertools.combinations(1a): replaced the SagePartitions+permutations+settriple with a pure-Python_compositions(m, k)generator that placesk-1dividers inm-1slots. Yields each of theC(m-1, k-1)compositions exactly once with no deduplication. On the heaviest case (d=5, n=3) this is 19× faster in isolation;sage.combinat.Compositionswas tried first but found to be 36× slower than the current code due to Sage type-system overhead.Hoist
.tuple()calls (1b): precomputedbasis_tuples = tuple(s.tuple() for s in basis_element)once per cached call instead of calling.tuple()inside both loop levels.Bitmask for
removed(1c): replaced theremoved: list(O(n) membership) with a small-int bitmask (O(1) bitwise AND). Arities in this benchmark are ≤ 5 so a Pythonintsuffices.
Impact. term_generator tottime: 16.5 s → 0.17 s (97× on this
function). Overall benchmark: 27.98 s → 11.52 s (2.43× wall-clock).
Table reduction is no longer in the top 10 by cumtime.
Remaining bottlenecks after this round¶
From benchmark_profile_2026-05-06_17-01-38_2_3_5_1.txt (serial,
-d2 -w3 -m5):
Function |
cumtime |
|---|---|
|
8.15 s |
|
7.93 s |
|
4.77 s |
|
4.16 s |
|
4.18 s |
|
3.10 s |
|
3.05 s |
|
2.85 s |
|
2.15 s |
The dominant remaining cost is the d_alpha / E-comodule / algebra-action
stack — the same cluster identified after the 2026-04-30 round but now
exposed without the table-reduction overhead masking it.
Optimizations applied 2026-05-07¶
Benchmark baseline (weight 4, deg_max 3, 8 jobs, parallel)¶
The serial benchmark is now fast enough that the parallel configuration
(-d2 -w4 -m3 -j8) is the relevant measurement point. The profile dumps
dump/benchmark_profile_2026-05-07_14-42-28_2_4_3_8.txt and
dump/benchmark_prewarm_profile_2026-05-07_14-42-28_2_4_3_8.txt were
analysed at the start of this session.
Wall-time breakdown at baseline (178.0 s total):
Phase |
Wall (s) |
|---|---|
Serial prewarm ( |
22.7 |
Worker compute (1065 s merged ÷ 8, approximate) |
~133 |
Pool teardown ( |
~134 |
The 134 s teardown is the single largest contributor: 8 long-lived workers accumulate ~5 degrees of dirty copy-on-write pages and large caches over their lifetimes. Attempting to bypass this with SIGKILL (Action A) was investigated and abandoned — see the note below.
Investigated and rejected: SIGKILL pool teardown (Action A)¶
Problem. pool.terminate() + pool.join() blocks for ~134 s while 8
workers exit, running Python finalizers and releasing accumulated COW pages.
Attempted fix. Send signal.SIGKILL to each worker pid before calling
terminate() to bypass finalizers.
Why it deadlocks. Workers blocked inside SimpleQueue.get() hold the
queue’s _rlock (a POSIX semaphore acquired during recv_bytes()).
SIGKILL skips the _rlock.release() call, leaving the semaphore locked.
pool._terminate_pool subsequently calls _help_stuff_finish, which tries
to acquire inqueue._rlock in order to drain pending tasks — this blocks
forever.
The deadlock was confirmed empirically: the test
test_boundary_matrix_parallel_fast_path_matches_serial hung indefinitely
after the SIGKILL change was introduced. multiprocessing.Pool is
structurally incompatible with instant worker kill from the parent. The
SIGKILL change was reverted. Possible future alternatives are worker
self-exit via os._exit(0) (avoids holding the lock) or
maxtasksperchild to recycle workers per few degrees rather than once at
the very end.
23. Cache FreeAlgebraModule._normalize_key and _boundary_on_basis¶
File: src/uconf/algebraic/free_algebra.py
FreeAlgebraModule._normalize_key (line 175) and _boundary_on_basis
(line 350) were the two remaining uncached methods in the class. All
analogous methods in the codebase (cofree_coalgebra._boundary_on_basis,
bar_construction._boundary_on_basis, etc.) already carry @cached_method.
_normalize_key is called from four independent sites: inside
_normalized_corolla_sum (from the algebra action stack), inside
_element_constructor_ for user-facing construction, inside _boundary_on_basis
itself (from the bar/cobar differential), and from _act_impl. Caching it
ensures that any (p_key, m_tuple) pair encountered across these paths is
normalised at most once, regardless of which call site triggers it first.
_boundary_on_basis is called via the module morphism set up in __init__
and is similarly shared across boundary evaluations in different degrees.
24. Prewarm pullback morphism cache before forking workers (O6)¶
File: src/uconf/constructions/bar_algebra.py
Problem. The merged worker profile showed
morphism.__call__ → configuration._on_element costing ~446 s merged
CPU (~24 s wall at 8×). This is the surjection comodule morphism
(built by _make_surjection_comodule_morphism) being applied to cobar
elements derived from _alpha_on_basis in each _act_on_basis_slice call.
The morphism result depends only on the cobar element (not on the algebra
inputs), so each of the 8 workers was independently computing and caching
the same set of morphism evaluations from scratch.
Concretely, this involved three nested caches that were cold in every worker at fork time:
PullbackAlgebra._morphism_cache— mapstuple(cobar_element)to the corresponding surjection Hadamard element.make_e_comodule_morphismclosure dictsroot_image_cache/tree_image_cache— memoize per-generator and per-subtree images of the BE-valued comodule map._compute_table_reduction_cached— global@cached_functionthat maps BE basis keys to surjection elements.
Fix. Extended _prewarm_parallel_boundary_caches to prewarm all three
caches in the parent process. For every unique (n_r, c_right_key) pair
already visited by the existing prewarm loop, after computing
alpha_elem = self._alpha_on_basis(n_r, c_right_key) (a cache hit), the
prewarm now calls:
if p_terms not in _morphism_cache:
_morphism_cache[p_terms] = _algebra_morphism(alpha_elem)
where _morphism_cache = self._algebra._morphism_cache and
_algebra_morphism = self._algebra.morphism. This single call transitively
fills all three downstream caches. Workers forked after prewarm inherit the
populated caches via Linux copy-on-write.
The prewarm is gated on hasattr(self._algebra, "_morphism_cache") so it
is a no-op for non-PullbackAlgebra algebras.
Expected savings. ~24 s wall (the per-worker first-fill cost eliminated
by COW inheritance). Additional reduction in term_generator /
_compute_table_reduction_cached first-fill tottime per worker (~6 s
tottime shared across 8 workers → ~1 s wall recovered).
Remaining bottlenecks after this round¶
From dump/benchmark_profile_2026-05-07_14-42-28_2_4_3_8.txt
(parallel, -d2 -w4 -m3 -j8, 1065 s merged CPU):
Top frames by tottime (where CPU is actually spent, not inherited from callees):
Function |
tottime (s) |
|---|---|
|
49.8 |
|
48.5 |
|
43.2 |
|
36.6 |
|
33.1 |
|
14.9 |
|
11.9 |
|
11.6 |
|
10.4 |
|
10.3 |
Note that the 48.5 s in term_generator and 43.2 s in _compositions
are worker-side first fills of _compute_table_reduction_cached. If the
new morphism prewarm also covers _compute_table_reduction_cached (it
does, transitively), these costs will move to the prewarm and be paid only
once, reducing worker tottime by ~24 s merged ≈ 3 s wall.
The RootedTree.__init__ (7.2 M calls, 49.8 s) is the largest remaining
single function. Opportunities: defer expensive derived-field computation
(_leaves, _weight) for callers that only need _hash or _arity; this
requires auditing all callers. Pool teardown (~134 s wall) remains the
dominant wall-time contributor until either action E (worker self-exit) or a
pool-replacement strategy is implemented.