Skip to content

perf(types): canonicalize acyclic type graphs bottom-up instead of by fixed point - #6375

Open
ggreif wants to merge 5 commits into
masterfrom
gabor/expgraph-acyclic-combine
Open

ggreif wants to merge 5 commits into
masterfrom
gabor/expgraph-acyclic-combine

Conversation

@ggreif

@ggreif ggreif commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Fixes the long-standing complaint that test/bench/typtbl.mo takes over two
minutes to run.

Diagnosis

The type checker is not implicated: moc --check on that file takes 0.031s.
All the time is codegen, and it is cubic in the nesting depth:

depth compile implied exponent
256 1.32s
512 10.01s n^2.92
768 36.83s n^3.21
1024 94.53s n^3.28

ExpGraph.combine finds the coarsest bisimulation by partition refinement,
restarting from the one-class partition and re-splitting every node on each
pass. A pass distinguishes one more level of structure, so a chain of n
distinct nodes needs n passes. typ_hash canonicalizes on every call, and
codegen names each generated @deserialize_go / @buffer_size function by the
hash of its type — so a type nested n deep is hashed once per level.

Fix

On an acyclic graph bisimilarity is just structural equality, which can be read
off bottom-up in a single topological pass. Cyclic types keep the fixed point,
where it is genuinely needed: type A = ?[A] and type B = [?B] denote the
same regular tree without being structurally equal.

depth before after
256 1.32s 0.76s
512 10.01s 0.55s
768 36.83s 0.97s
1024 94.53s 1.63s

test/bench/typtbl.mo as a whole: 2m16s → 2.5s.

Verification

  • Output is unchanged. wasm2wat of typtbl.mo before and after is 390,978
    lines each, differing only in the embedded version string — not one
    instruction.
  • dune runtest mo_types passes, including the monolist / polylist cases
    that exercise the cyclic path.
  • No pessimisation. test/run (312 programs, cleaned before each run, quiet
    machine): 163.50s with the patch vs 164.49s without (real), 130.02s vs
    131.42s (user) — a wash, within noise.
  • nix build .#common-tests .#gc-tests passes locally.

🤖 Generated with Claude Code

… fixed point

`ExpGraph.combine` finds the coarsest bisimulation by partition refinement,
restarting from the one-class partition and re-splitting every node on each
pass. A pass distinguishes one more level of structure, so a graph that is a
chain of `n` distinct nodes needs `n` passes of O(n log n) work. `typ_hash`
canonicalizes on every call, and codegen names each generated
`@deserialize_go` / `@buffer_size` function by the hash of its type, so a type
nested `n` deep is hashed once per level -- making compilation cubic in the
nesting depth.

On an acyclic graph bisimilarity is just structural equality, which can be read
off bottom-up in a single pass: in topological order a node's class follows
from its label and its children's already-final classes. Take that path when
the graph has no cycle and keep the fixed point for recursive types, where it
is still needed (`type A = ?[A]` and `type B = [?B]` denote the same regular
tree without being structurally equal).

`test/bench/typtbl.mo` nests arrays up to 1024 deep:

    depth   before    after
      256    1.32s    0.76s
      512   10.01s    0.55s
      768   36.83s    0.97s
     1024   94.53s    1.63s

and the benchmark as a whole goes from 2m16s to 2.5s. Type checking was never
implicated -- `moc --check` on that file takes 0.03s.

Output is unaffected: `wasm2wat` of `typtbl.mo` before and after differs only
in the embedded version string.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ggreif ggreif self-assigned this Sep 16, 2026
Comment thread src/lang_utils/expGraph.ml Outdated
Comment thread src/lang_utils/expGraph.ml Outdated
Comment thread src/lang_utils/expGraph.ml Outdated
ggreif and others added 2 commits September 16, 2026 15:41
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Comparing from 01aee06 to f5836dc:
The produced WebAssembly code seems to be completely unchanged.
In terms of gas, no changes are observed in 5 tests.
In terms of size, no changes are observed in 5 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread src/lang_utils/expGraph.ml Outdated
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ggreif
ggreif marked this pull request as ready for review September 16, 2026 14:07
@ggreif
ggreif requested a review from a team as a code owner September 16, 2026 14:07
@github-actions

Copy link
Copy Markdown
Contributor

Cursor AI review

👍 APPROVE — looks safe to merge

Category Assessment Details
Summary Speeds up ExpGraph.combine by reading coarsest classes bottom-up on DAGs (topo_order + combine_acyclic) and keeping the existing fixed-point algorithm only when topo_order finds a cycle.
Code Quality Reuses rename, start_counting, and IM; combine_cyclic is the previous combine body unchanged. No new public API or speculative helpers.
Consistency Matches existing expGraph.ml style (ref cells, IM, polymorphic compare keys) and leaves expGraph.mli unchanged.
Correctness Traced canonicalizetyp_hashcompile_enhanced.ml / compile_classical.ml alias tags and @buffer_size/@deserialize_go names, plus ir_passes/show.ml and eq.ml. On a DAG, bottom-up (label, child classes) is the coarsest bisimulation; the 0↔root swap only permutes class ids so renumber still starts at node 0. Cyclic graphs still take the old path (typ_hash monolist/polylist).
Tests No .mo/.ok pairs in the diff, which matches a behavior-preserving hash. Cyclic fallback still covered by typ_hash.ml monolist/polylist; common acyclic types are the test/run path through typ_hash.
Changelog Internal compile-time optimization only (no language/prelude/CLI surface). Changelog.md is untouched, so no frozen-section edit.

Verdict

Decision: APPROVE
Risk: Low
Reason: Single-file DAG specialization of type-graph combine that computes the same partition as the existing fixed point, then relies on unchanged renumber; cyclic types keep the old algorithm. Hash-sensitive codegen/stable-alias uses stay observationally the same.


Generated for commit f5836dc

@ggreif
ggreif enabled auto-merge September 16, 2026 14:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant