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 = 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×

d_sigma calls

132,567

5,829

d_sigma cumtime

9.45 s

0.70 s

validate_tree calls

113,556

78,269

validate_tree cumtime

3.88 s

2.83 s

RootedTree.__init__ calls

187,888

150,817

e_comodule pipeline cumtime

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: call on_basis(key) directly instead of boundary(elem)

  • _normalized_corolla_sum: call _planarize_on_basis(key) directly instead of planarize(term(key))

  • d_sigma_decompose: bypass both boundary and planarize wrappers

  • free_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_tree in cobar compose for internally-grafted trees.

  • Use self.module.term(key) instead of self.module(key) in hadamard_algebra._act_impl for keys that are already validated.

  • Optimise surjection._validate_basis_key to 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

e_comodule pipeline

3.6 s

45 % — recursive cooperad coaction

hadamard_algebra._act_impl

2.6 s

Cartesian-product term expansion

free_algebra._act_impl

1.8 s

Operadic algebra action

_compute_table_reduction_cached

0.85 s

Partition enumeration (one-time)

sum_of_terms

1.3 s

83 K calls — Sage element construction

_boundary_matrix (total)

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_method since the 2026-04-08 round).

  • GF(2) sign short-circuit: ❌ Not worthwhile. Profiling shows sign_from_exponent and koszul_sign_of_permutation together 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_vertex calls (~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 via d_sigma_decompose (§8) and recursive _planarize_subtree.

New leads

  • E-comodule morphism caching or algebraic reformulation: The recursive _nu_on_planar.recurse function (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_terms overhead: With 83 K calls at 1.3 s, the Sage CombinatorialFreeModule.sum_of_terms method is a significant overhead. For internal constructions that produce validated keys, directly building the _from_dict representation could bypass the sum_of_terms_from_dict__classcall_private__ chain.

  • Table reduction algorithm: The current term_generator (0.8 s) enumerates all permutations of integer partitions via set(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_matrix loop 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

benchmark_detailed.py full chain complex

7.79 s

4.29 s

benchmark.py (cProfile) total time

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:

  1. recomputing the cumulative permutation product sigma_k * ... * sigma_1 from scratch at every recursion depth, and

  2. rebuilding the same generator/subtree images many times while traversing cobar trees with repeated decorations.

This was addressed in three places:

  • _nu_on_planar.recurse now threads the cumulative permutation product through the recursion instead of recomputing it from sigma_bar.

  • The equivariant cooperad action uses direct _permute_on_basis fast paths when available, avoiding repeated element-wrapper permute() overhead.

  • make_e_comodule_morphism memoizes 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

_extend_tree cumtime

4.59 s

2.18 s

e_comodule_on_generator cumtime

3.76 s

1.76 s

_nu_on_planar cumtime

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_planar now memoizes repeated d_sigma_decompose calls, repeated cooperad-factor permutations, and repeated rho(sigma_bar) evaluations inside one recursion tree.

  • CofreeCoalgebraModule._normalized_corolla_sum and _boundary_on_basis now accumulate directly into dictionaries instead of repeatedly doing result += ....

  • _boundary_matrix now 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

benchmark_detailed.py full chain complex

19.58 s

0.056 s

benchmark_detailed.py arity-3 E-comodule sub-benchmark

5.22 s

1.67 s

benchmark_detailed.py basis enumeration

0.407 s

~0.000 s

benchmark.py wall time (n_jobs=8)

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 cumulative

  • configuration._on_element: 88.4 s

  • e_comodule_on_generator: 70.7 s

  • e_comodule._on_element: 74.2 s

  • hadamard_algebra._act_impl: 28.6 s

  • hadamard_algebra._act_on_basis_tuple: 28.0 s

  • free_algebra._act_impl: 24.4 s

  • free_algebra._act_on_basis_tuple: 23.1 s

  • quasi_planar.d_sigma_decompose: 18.8 s

  • cofree_coalgebra._boundary_on_basis: 13.0 s

  • cofree_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×

term_generator tottime

16.50 s

0.17 s

_compute_table_reduction_cached cumtime

16.64 s

0.49 s

sum_of_terms cumtime

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:

  1. Composition enumeration via itertools.combinations (1a): replaced the Sage Partitions + permutations + set triple with a pure-Python _compositions(m, k) generator that places k-1 dividers in m-1 slots. Yields each of the C(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.Compositions was tried first but found to be 36× slower than the current code due to Sage type-system overhead.

  2. Hoist .tuple() calls (1b): precomputed basis_tuples = tuple(s.tuple() for s in basis_element) once per cached call instead of calling .tuple() inside both loop levels.

  3. Bitmask for removed (1c): replaced the removed: list (O(n) membership) with a small-int bitmask (O(1) bitwise AND). Arities in this benchmark are ≤ 5 so a Python int suffices.

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

bar_algebra._dalpha_all_splits

8.15 s

pullback_algebra._act_on_basis_inputs

7.93 s

configuration._on_element

4.77 s

e_comodule_morphism._root_image_for_generator

4.16 s

e_comodule_morphism._extend_tree

4.18 s

hadamard_algebra._act_on_basis_tuple

3.10 s

e_comodule_morphism._nu_on_planar

3.05 s

e_comodule_morphism.recurse

2.85 s

free_algebra._act_on_basis_tuple

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 (_prewarm_parallel_boundary_caches)

22.7

Worker compute (1065 s merged ÷ 8, approximate)

~133

Pool teardown (pool.terminate() + join())

~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 — maps tuple(cobar_element) to the corresponding surjection Hadamard element.

  • make_e_comodule_morphism closure dicts root_image_cache / tree_image_cache — memoize per-generator and per-subtree images of the BE-valued comodule map.

  • _compute_table_reduction_cached — global @cached_function that 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)

trees.RootedTree.__init__

49.8

__init__.term_generator

48.5

__init__._compositions

43.2

free_module._from_dict

36.6

list.append

33.1

e_comodule_morphism.recurse

14.9

shifted_operad.permute

11.9

hadamard_algebra._act_on_basis_tuple

11.6

bar_construction._planarize_subtree

10.4

cobar_construction.compose

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.