Count free references incrementally in the IR optimizer#189
Merged
Conversation
optimizeModule recomputed the whole-module free-reference map for every Standalone binding that reached the use-once check, costing O(bindings × module size) per run and repeating inside the optimize+dce fixpoint (#142). Those counts also mixed the original bindings with the already-substituted exports — a stale view that misjudged use-once and could duplicate a binding across several sites or skip a legitimate inline (#143). Thread a single free-reference map through the right-to-left fold, initialised from the exports and updated by a known delta as each binding is kept or inlined (see Note [Incremental free-reference counting]). Because a binding is only referenced by material to its right, its count is final by the time it is visited, so one incremental map replaces the per-binding recomputation and reflects the live post-substitution module. Adds a single-run regression spec that goes red on the stale count. The structural goldens shift (fewer duplicated foreign accessors, e.g. Data.Ring.intSub in Fibonacci; gensym renumbering elsewhere); the eval goldens are unchanged, so runtime behaviour is preserved.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
optimizeModuledecides whether to inline eachStandalonebinding partly on whether it is referenced exactly once. It computed those reference counts by foldingcountFreeRefsover every expression in the module, and it redid that fold for every binding that reached the use-once check. That is O(bindings × module size) peroptimizeModulerun, and the run repeats on every iteration of the optimize+dce fixpoint (#142).The same counts were also assembled from the original bindings plus the already-substituted exports, a mix of stale and fresh sources. Within a single run that misjudges use-once: a binding that has become referenced multiple times can still be inlined into all of them (code bloat), or a genuinely used-once binding can be skipped.
idempotentlypapered over it across iterations at the cost of extra passes (#143).What changed
A single free-reference map is threaded through the fold with a precise invariant: it always holds the free-reference counts over the current accumulator (the bindings kept so far plus the exports).
foldrMvisits bindings right-to-left, and a binding can only be referenced by material to its right, so by the time a binding is visited every one of its referencers is already accounted for and its count is final. The map is built once from the exports and updated by a known delta at each step: add a kept binding's own refs, or, when inlining, add one copy of the inlinee's refs per replaced occurrence and drop the binding's own entry. Note [Incremental free-reference counting] inOptimizer.hsrecords the invariant and the delta.This removes the quadratic recomputation and, because the counts now track the live post-substitution module, fixes the stale use-once judgement in the same change.
Measurements
Allocation of a single
optimizeModulerun on a synthetic module of N non-inlinable bindings, via ghci:set +s:Doubling N multiplies allocation by about 2.6x to 3.2x before (trending toward the quadratic 4x) and about 1.7x to 1.8x after (linear). The gap widens with module size, as expected going from O(bindings × size) to O(size).
Goldens
Structural goldens (
golden.ir/golden.lua) shift for 28 modules. Two kinds of change: the live count stops duplicating a multiply-used foreign accessor (for exampleData.Ring.intSubis now shared rather than pasted into both arms ofGolden.Fibonacci), and gensym suffixes renumber where a different number of substitutions runs. Everyeval/golden.txtoracle is unchanged, so runtime behaviour is preserved.Testing
Adds a single-run regression spec that inlines a binding which becomes used-once mid-run; it is red against the stale count and green with the fix. A single run is deliberate: the optimize+dce fixpoint masks the misjudgement by self-correcting on a later iteration. The existing optimizer property tests (free references preserved, GUC invariants, idempotence, bound-name set) still pass, and the full suite is green with the eval oracles untouched.
Fixes #142
Fixes #143