-
cc533b66
by Sasha Bogicevic at 2026-10-31T09:18:15-04:00
Don't report -XStrict-generated bangs under -Wredundant-bang-patterns
The -XStrict flag causes the desugarer to insert bang patterns.
These are not user-written, so -Wredundant-bang-patterns must not
report them.
To achieve this, the desugarer now tracks whether it is desugaring
compiler-generated code (similar to the typechecker's 'tcl_in_gen_code').
Bangs inserted by -XStrict are given generated spans, which suppresses
the "redundant bang" warnings without otherwise affecting pattern-match
warnings.
Fixes #27323
-
13d38047
by Luite Stegeman at 2026-10-31T09:19:11-04:00
rts: tombstone Weak# copies that lose an evacuation race (#23160)
Mark the losing copy of a Weak# during an evacuation race in parallel
GC with stg_DEAD_WEAK_info. This ensures that the nonmoving GC
doesn't follow dangling pointers, leading to crashes.
Fixes #23160
-
63347328
by Zubin Duggal at 2026-10-31T09:19:52-04:00
ghci: Drop GHCi's exception context when rethrowing in ghc -e
Fixes #27847.
-
a85fef3f
by Zubin Duggal at 2026-10-31T09:19:52-04:00
base: Preserve exception context in modifyIOError
Fixes #27851.
CLC proposal: https://github.com/haskell/core-libraries-committee/issues/446
-
1d60b591
by Zubin Duggal at 2026-10-31T09:19:52-04:00
base: Preserve exception context in mapException
Fixes #27851.
CLC proposal: https://github.com/haskell/core-libraries-committee/issues/446
-
3c029a1b
by Zubin Duggal at 2026-10-31T09:19:52-04:00
base: Preserve exception context when rethrowing exceptions in Handle reads
Fixes #27875.
CLC proposal: https://github.com/haskell/core-libraries-committee/issues/447
-
ab2b317a
by Zubin Duggal at 2026-10-31T09:19:52-04:00
base: Preserve exception context when rethrowing exceptions in hClose
Fixes #27875.
CLC proposal: https://github.com/haskell/core-libraries-committee/issues/447
-
f5abf277
by Zubin Duggal at 2026-10-31T09:19:52-04:00
base: Preserve exception context when rethrowing exceptions in Windows openFile
Fixes #27875.
CLC proposal: https://github.com/haskell/core-libraries-committee/issues/447
-
d842c672
by Zubin Duggal at 2026-10-31T09:19:52-04:00
base: Preserve exception context when rethrowing exceptions in executablePath
Fixes #27875.
CLC proposal: https://github.com/haskell/core-libraries-committee/issues/447
-
c5a8bb9f
by Simon Peyton Jones at 2026-10-31T09:20:34-04:00
Simplify a tc-trace which could crash the compiler
The trace formatted an error message, which prematurely
read the ExpType in a FunResCtxt.
Solution: don't put call the error message formatting
when tc-tracing.
-
ab49b1d2
by Simon Peyton Jones at 2026-10-31T09:20:34-04:00
Tiny comment change to uniqAway
-
6efe4ba3
by Simon Peyton Jones at 2026-10-31T09:20:34-04:00
Make niFixSubst idempotent
I discovered (when fixing #27994) that `niFixSubst` didn't actually
generate an idempotent subsitution despite its claims. It could
return a substitution that mentions domain variables on in its range,
when those variables are hidden inside the kind of a type variable
occurrence. As a result it can generate ill-kinded types. Boo!
The fix is simple, and uses deep substitution.
See Note [Finding the substitution fixpoint]
I don't think it actually /caused/ any bugs, so there is no
regression test.
-
3b60ae7e
by Simon Peyton Jones at 2026-10-31T09:20:34-04:00
Fix a nasty bug in fundeps
This MR fixes #27994, which turned out to be very tricky.
See Note [Exploiting closed type famililes], especially (CF7). I was
ending up with TyVars (not TcTyVars) in the solver, because the
substitution computed in `getRelevantBranches` mixed the two.
The code was just too clever. I ended up simplifying it quite a bit.
For /closed/ families we no longer call `trimQtvs` (was `trim_qtvs`).
Open families still do use `trimQtvs`. See Note [trimQTvs]
This patch also fixes one other buglet in the same code.
* Over-saturated type families were being mis-handled.
See the new use of `saturatedFamApp_maybe`.
-
e4610f2d
by Facundo Domínguez at 2026-10-31T09:21:24-04:00
Have haddock --incremental look for the module in -this-unit-id
-
bc6c0db6
by sheaf at 2026-10-31T09:22:20-04:00
mightBeFunTy: hotfix for #27979
ce01ccb6 refactored 'GHC.Types.RepType.mightBeFunTy' to look through
unary classes. That change introduced the helper function 'unwrap_type'
which was supposed to always return an unwrapped type, but its fallback
equation mistakenly returned the original type. This caused the
subsequent 'tyConAppTyCon_maybe' to fail in cases where it should
succeed (e.g. looking through foralls).
In this commit, we move all the unary class unwrapping logic into
'unwrapType' where it belonged from the start, and update the
documentation of 'unwrapType' to better explain what it should be used
for (#26695).
Fixes #27979 #26695
-------------------------
Metric Increase:
T5642
-------------------------
-
233cce46
by Alan Zimmerman at 2026-10-31T09:23:01-04:00
EPA: Tidy up ExactPrint.markAnnListA'
It takes an action which can modify the AnnList, but this
is never used. So remove it.
-
3594a908
by Sylvain Henry at 2026-10-31T09:23:51-04:00
gitmodules: ignore dirty libffi-clib submodule (#26785)
`./boot` runs autoreconf in libraries/libffi-clib, which leaves
`configure~` behind and rewrites the tracked `aclocal.m4` whenever the
local automake version differs from the one that generated it. Every
checkout then looks dirty. Ignore working-tree changes in the submodule
(`ignore = dirty`); a changed submodule commit is still reported.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
-
341223c0
by Rodrigo Mesquita at 2026-10-31T09:24:33-04:00
refactor: Keep the interactive Ids directly rather than Name
Removes things like
names <- map idName ids
map (\i -> let id = find (\x -> i==idName x) names) idso
and is more straightforward because there's less state going around (no
more updating HscEnv as an output)
It also kills `printTypeOfNames` completely, since now we just have the
Ids. No longer `lookupName` names to get back the ids...
```
printTypeOfName :: GHC.GhcMonad m => Name -> m ()
printTypeOfName n
= do maybe_tything <- GHC.lookupName n
case maybe_tything of
Nothing -> return ()
Just thing -> printTyThing thing
```
now is just `printTyThing . AnId`
Also:
```diff
- tythings <- catMaybes `liftM` mapM GHC.lookupName namesSorted
- docs <- mapM pprTypeAndContents [i | AnId i <- tythings]
+ docs <- mapM pprTypeAndContents idsSorted
```
(and yes, in ExecBreaks there are only Ids, no TyThings, bc they are
only constructed by the interpreter execution which can only bind ids.
for TyThings use runDecls which doesn't return execBreaks.)
There's some care to be had to make sure we use the "most refined" Ids,
whereas when using `Name`s we'd always fetch the most refined Id from
scratch.
Fixes #28052
-
54cf3c1e
by sheaf at 2026-11-03T07:00:23-05:00
Consolidate all CPU feature handling
This commit introduces GHC.Platform.CpuFeatures which consolidates all
of the logic for implications between CPU feature flags (mainly X86):
- DynFlags now only records the set of -m<feature> flags that were
passed, as an EnumSet CpuFeature. That keeps it a simple record of
the flags the user passed, with no post-processing.
- GHC.Platform.CpuFeatures.initCpuFeatures turns this set into a
CpuFeatures value, applying all implications and the per-architecture
baseline. This is the single home for all of the feature implication
logic.
Some datatypes were reworked to better reflect the implication
structure in the types, e.g. SseAvxVersion has a strategically
placed 'FMA' boolean.
- The NCG, LLVM backend, StgToCmm, CPP defines and LLVM -mattr
flags all consult CpuFeatures, via targetCpuFeatures.
See Note [CPU features], which replaces
Note [Implications between X86 CPU feature flags].
Behaviour changes:
- x86 feature flags (-msse3, -mavx, -mavx512*, -mgfni, ...) passed
when targeting a non-x86 architecture no longer cause x86 CPP
macros (__SSE3__, __AVX512BW__, __GFNI__, ...) to be defined, nor
x86 attributes to be passed to LLVM.
- Error messages reported when required CPU features are not enabled
now print out the minimal set of flags required
(e.g. "-mavx512vl" rather than "-mavx512f -mavx512vl").
-
9af64305
by Luite Stegeman at 2026-11-03T07:01:23-05:00
rts: move stack check to the start of the stg_unpack_cstring thunks
fixes a deadlock
see #27272
-
a2208b21
by Simon Jakobi at 2026-11-03T07:02:05-05:00
Cmm: exclude the entry block from common block elimination
a1d8139054c had fixed #27722 only partially, because it didn't prevent
elimination of the entry block in later rounds, where the entry block is
no longer guaranteed to come first.
To close this loophole, we exclude the entry block from the CBE merge
logic. See Note [Retain entry block during common block elimination] for
details.
This also deletes a misleading comment regarding unreachable blocks: the
control-flow optimiser doesn't actually guarantee that no unreachable
blocks reach CBE.
Fixes #27722.
Assisted-by: Claude Fable 5.1
-
37ad4710
by Alan Zimmerman at 2026-11-03T07:02:46-05:00
EPA: Use AnnList for GADT declarations
Continuing the process of using AnnList to capture parser layout
-
a7cdfe96
by Mikolaj Konarski at 2026-11-03T22:23:56+01:00
rts: run finalizers soon after the GC that finds them
scheduleFinalizers puts the finalizer thread at the end of the run
queue, and the thread whose heap overflow caused the GC goes back
to its front unless a context switch is pending. So under the RTS timer
the finalizers wait for the next context switch, 20 ms by default,
and whatever they would free stays live until then: a program that opens
and closes handles fast keeps every closed handle's buffers. nofib's
maillist, which appends to its output a line at a time, peaks at 18 MB
of residency where -V0 and -C0, whose scheduler switches whenever
another thread is runnable, keep 1.65 MB.
Request a context switch when scheduling the finalizers, so that they
run at the next block boundary. maillist then keeps 1.65 MB
under the default timer and runs 13.2% fewer instructions. Across
nofib, cacheprof runs 1.2% more and gg built at -O0 0.32% more, as each
does under -V0: run promptly, the finalizers lower the residency
(cacheprof's peak 0.82 MB against 0.90 MB), so the major GCs, triggered
by the old generation's size against the live data, come more often
(cacheprof's 148 against 135). No other program moves by 0.2% beyond
its run-to-run noise.
Fixes #28060.
Assisted-by: Claude Opus 5.5
-
bbe30891
by Mikolaj Konarski at 2026-11-03T22:23:56+01:00
Reassociate in Cmm only where literals fold, and narrow != as ==
GHC.Cmm.Opt's special case (a - b) + c ==> a + (c - b) fired whatever b
and c were, and where c - b could not fold it put a literal on the left
of a subtraction, which x86 cannot encode as an immediate: containers'
Set.elemAt, at -O2, computed i + ((-1) - s) for its right step, a mov,
a sub and an add where a sub and a dec do. It now fires only where b
and c are literals. The reassociation (a + b) - c ==> a + (b - c) had
the same flaw where b is a literal and c is not, turning (x * y + 1) - w
into x * y + (1 - w), and now fires for a subtraction only where c
is a literal. And the narrowing that compares a widened operand
with a literal at the operand's width listed MO_Eq but not MO_Ne,
so a byte widened to a word was tested with != after a zero extension;
MO_Ne is added beside MO_Eq. A comparison of two operands widened alike,
from the same width and both by zero extension or both by sign
extension, is now done at that width as well, under the literal case's
rule for signedness (#2080), its tables moved to the top level to serve
both.
The tests cmm_fold_lit (Cmm, the reassociation and the narrowing)
and cmm_fold_elemAt (Haskell at -O2, the special case) fail without
these changes.
The gains: containers' set alterF:four, map elemAt and nubOrd
noFusion_distinct, where (a - b) + c is now kept; nofib's simple
and pic, where (a + b) - c is; vector's awshcc and nofib's
reverse-complement, lambda and parser, where != is narrowed like ==;
and the boot libraries' comparisons of two operands widened alike,
in text, bytestring and ghc-internal's eqWord32, containers running
0.02% fewer instructions by them.
FIGURES FROM THE REBUILD
Assisted-by: Claude Opus 5.5
-
f4516e61
by Mikolaj Konarski at 2026-11-03T22:23:56+01:00
Predict an inequality with a literal in static branch prediction
ohPredicts, static branch prediction's opcode heuristic, predicts
that a comparison with a literal is unlikely to come out equal,
but it matched only MO_Eq. The control-flow optimisation turns
an equality into an inequality where that lets the test's true side fall
through (Note [Invert Cmm conditionals] in GHC.Cmm.ContFlowOpt),
and so spelt the same test went unpredicted, its two sides weighted
alike. ohPredicts now predicts the true side of an inequality
with a literal (0.7), as it predicts the false side of an equality,
except where an operand is masked by MO_And, as in a pointer tag test,
(x & 7) != 0, or a test of a bitmap's bits, or is a constructor tag: one
read from an info table, an unboxed sum's, or a local register every
assignment to which is a tag, which a pass over the procedure's Cmm
finds. Whether those bits are set, or which constructor a tag names,
depends on the type or the data, which the CFG does not record,
so a test of a constructor tag is predicted as before (Note [Static
prediction of tag tests]). Predicting every inequality made containers'
benchmarks take up to 3.1% more instructions, through the tag tests
of tree lookups; leaving out only pointer tag tests, intmap fromSet took
1.3% to 1.8% more, through buildTree's bitmap test.
What this removes is a tie. In megaparsec's sepEndBy loop, a cold
continuation's edge into the loop's second head weighs as much
as the hot edge into it, 0.17366 each once "Hand calcFreqs a procedure's
loops inner first" estimates them, because the test before them, written
as != 0, is estimated 50/50; the tie gives the cold block
the fall-through, and every element a jump. With this commit the hot
edge is the heavier one.
The trade is k-nucleotide's hash table lookup: the exits of its bucket
loop, written as tests for zero, come out as != 0 and are now predicted
to stay in the loop, so that with the loop-exit heuristic the loop's
back edge outweighs its entry; block layout gives the back edge
the head's fall-through, and every first probe, which finds its key
about 95% of the time, pays a jump. The commit that lets a loop's back
edge outrank its entry makes this layout anyway. The gains include
containers' intmap compareSize:n (3.45% fewer instructions), intmap
fromListWith:randomDups (0.18% to 0.21% fewer) and nubOrd
issue1202_repeat (0.89% fewer at -O1), and megaparsec's endBy (0.62%
fewer).
TODO: the exception goes by an operand's form, not by what decides
the test: a parity test, its mask inside arithmetic, is predicted,
and an evaluation test, (x & 7) != 0, is left out
with the constructor-tag tests; predicting evaluation tests
as evaluated, in either spelling, cost nofib 0.36% more instructions,
and leaving parity tests out is still to be measured.
FIGURES FROM THE REBUILD
Assisted-by: Claude Opus 5.5
-
13aa74d4
by Mikolaj Konarski at 2026-11-03T22:23:56+01:00
Find a CFG's back edges by one depth-first search
findBackEdges classified every edge of the CFG (classifyEdges, which
timestamps the visits of a depth-first search from the entry) to find
the few that lead back to a block on the search's path. optHsPatterns
calls it on every procedure, at every optimisation level, to give back
edges their bonus. findBackEdges is now that depth-first search itself:
it visits the successors in the same order and collects, as it goes,
the edges into a block still on its stack, leaving out an edge
from a block to itself, which classifyEdges calls a self loop rather
than a back edge. classifyEdges and its EdgeType,
in GHC.Data.Graph.Directed, had no other user and are gone.
The generated code is the same: the assembly of perf/compiler's T3294,
T12707, T783 and T9961 at -O0 and -O, and the object code of nofib's
programs at -O2, are the parent's. The compiler allocates 0.2% less
compiling T3294 at -O0, 0.7% less compiling T12707, 1.0% less compiling
T783 and 1.0% less compiling T9961; at -O, 0.3% less compiling T12707
and 0.2% less compiling T783; and compiling nofib's programs at -O2,
0.20% less in geometric mean and up to 0.36% less (real/compress2).
Compiling !16269's ManyBasicBlocks at -O1 and -O2, the compiler
allocates 0.2% less.
Assisted-by: Claude Opus 5.5
-
4dd2a50d
by Mikolaj Konarski at 2026-11-03T22:23:56+01:00
Drop jumps over blocks dropJumps empties
dropJumps removes a block's last jump where it can fall through
to the next block, and a jump-only block it so empties stays as a label,
so that a jump over it to the block after is left as a jump to the next
instruction (x86-64's eb 00, which master's intmap benchmarks at -O1
carry). It now looks past the blocks that are empty or that it leaves
empty, keeping its forward recursion: a foldr from the end, tried first,
made the compiler allocate 0.12% more compiling perf/compiler's T783
at -O0, building the whole list before the first block.
FIGURES FROM THE REBUILD
Assisted-by: Claude Opus 5.5
-
d3ce9189
by Mikolaj Konarski at 2026-11-03T22:23:56+01:00
Let a loop's back edge outrank its entry in block layout
sequenceChain lays the blocks out by fusing chains along the heaviest
edges first, by the final edge weights. Under -fcmm-static-pred,
on at -O1 and -O2, those are the frequencies static prediction
estimates, which can rate a loop's back edge below its entry: where
the branch that decides whether the loop goes on has no back edge among
its targets, the other heuristics predict it, and they can predict
staying in the loop to be unlikely, as in a lookup down a tree (Note
[Static prediction of tag tests]). The entry then falls through
into the loop's head, and every iteration pays a jump back.
optHsPatterns gives back edges a bonus (increaseBackEdgeWeight),
but static prediction replaces the weights of the branches
its heuristics cover with its predictions, so that the bonus counts only
at those it leaves alone, a heap or stack check's and those whose
targets are all back edges. sequenceChain now raises (preferBackEdges)
each back edge by a little more than the weight of the heaviest edge
into its head that is not a call, a back edge or a self loop, which
keeps the order of several back edges into one head; Note [Layout
relevant edge weights]. A back edge out of a jump table is not raised:
it cannot fall through, so raising it would only take the head's
fall-through from an edge that can. Nor is a back edge out of a block
with another successor: that block executes its branch whichever
successor falls through, so falling through saves nothing an iteration,
while every entry pays a jump; leaving such edges out takes 1.55% off
intset folds:sparse foldr_elem and 0.78% off foldl_elem, and 6.4% off
folds:dense foldl'_maximum, at -O2. preferBackEdges takes the back edges
from findBackEdges, now exported.
The garbage collector's return to the block that made a heap or stack
check is no such loop: a back edge leaving a block that follows a failed
check stays as it is. checkFailures, in GHC.CmmToAsm.CFG, finds
those blocks: the targets of the less likely edges of a branch
that getCfg tags HeapStackCheck, one whose condition reads SpLim, HpLim
or BaseReg, and their successors, such as the block the garbage
collector returns to, which can be a continuation that a call returns
to as well. A block that follows such a block and whose one out-edge has
the code generator as its source, such as a block the register allocator
puts there, counts as following a failed check too (coldBlocks). Raising
those back edges too makes containers' benchmarks take 0.28% more
instructions (set fromAscList:fusion 3.2%, intset fromList:worstCase
3.0%); leaving out the blocks that follow a failed check but
not the block the register allocator puts after such a block, 0.09% more
(map alter insert 0.9%). A new Note, [Frequencies before register
allocation] in GHC.CmmToAsm.CFG, names two shapes coldBlocks misses:
the block allocMoreStack splits off a block the garbage collector
returns to, and a fixup block on a failure edge, which takes the edge's
tag. A TODO at coldBlocks points there. Building the set in GHC.CmmToAsm
instead, from checkFailures' blocks, the blocks allocMoreStack splits
off a cold entry block and the fixup blocks on an edge out of or
into a cold block, changed a few procedures' layout in nofib and moved
no figure measured, while costing up to 0.03% more compile allocation
at -O0.
Nor is a back edge into the one successor of the procedure's entry block
raised: the entry would then jump to that block at every call, which
a loop that seldom iterates does not win back. The RTS's entry code has
such loops, which retry on an indirection: raising their back edges made
stg_BLACKHOLE_info, stg_enter_info and stg_forceIO_info begin
with a jump into the loop, so that stg_BLACKHOLE_info ran an instruction
more for every blackhole entered. Where the code generator split
the entry block, as allocMoreStack does, and the block holding the rest
of its code has one successor, that successor stands in for the entry's.
Measured against the parent commit, in instructions; sequenceChain runs
only under -fblock-layout-cfg, on at -O1 and -O2. At -O2 nofib's
programs take 0.06% fewer (queens 1.7%, wheel-sieve1 1.0%), vector's
benchmarks 0.44% fewer (leaffix 3.4%, rootfix 2.7%, listRank 2.2%),
containers' 0.19% fewer and megaparsec's 0.25% fewer (skipMany 2.2%,
many 1.0%); at -O1 nofib's take 0.06% fewer (queens 1.4%, knights 1.3%,
wheel-sieve1 1.0%); fannkuch-redux moves by no more than its noise.
In containers and megaparsec the rises are trades. The rule's own
is the jump a loop's entry now pays, which its iterations seldom win
back: intmap lookup_mixed 2.0%, whose lookups gain nothing
from the descent that now falls through, and intset's intersection
and difference benchmarks, 0.7 to 0.8%: the continuation that evaluates
the second set now jumps into differenceTip's descent, a loop whose head
has two back edges and which runs no iteration where that set is a Tip,
as in these benchmarks, while Map's and IntMap's lookups, of the same
shape, iterate about sixteen times an entry and gain 2.3% to 3.1%;
no static rule tells the two apart. containers' restrictKeys,
withoutKeys and graphFromEdges n=15, and megaparsec's hexadecimal,
decimal and octal at -O1, pay the same jump, and so does megaparsec's
scientific.4000, which calls decimal's decimal_: 20 instructions more,
as decimal.4000.
The fused rows, containers' map fromDistinctDescList:fusion, map and set
fromList-distinctAsc:fusion, and map fromListWith-asc,
fromListWithKey-asc and their :fusion rows, take 0.3% to 0.8% more
at -O2, and set fromDistinctDescList:fusion 0.89% more. Raising back
edges only into heads outside every enclosing loop reverts them,
and with them nubInt's noFusion_repeat, which pays the same jump
into IntSet's member, 1000 instructions a run, and issue1202_repeat,
intmap lookup_mixed and intset alterF:delete:present; but it gives back
the gains of intmap lookup_hits, lookup_most and index (3.0%),
lookup_misses (2.6%), intset member and alterF:member (1.0% to 1.2%),
and vector's listRank (2.5%) and awshcc (1.8%), at -O2.
Where two back edges raised alike into one head tie, the static order
breaks the tie: at -O1 megaparsec's reachOffset pays 80000 instructions
a call, isWideChar's loop head having two back edges, tied, and the tie
going by label order to the one that ASCII input never takes. Flipping
the order takes 4.0% off intmap lookup_half, 2.0% off lookup_mixed
and 1.2% off intset alterF:delete:present, and at -O1 1.1% off
megaparsec's reachOffsetNoLine and 0.8% off errorBundlePretty, but adds
2.5% to nofib's fasta at -O2 and 1.2% at -O1, gives back 96% of banner's
gain, and adds 5.9% to intset fromAscList:fusion, 2.3% to fromAscList,
1.3% to mapMonotonic and, at -O1, 0.9% to vector's next
and prevPermutationBijective; the static order is kept.
No benchmark's allocation changes beyond nofib's run-to-run noise.
The compiler allocates 0.1% more compiling perf/compiler's T12707
and T783 at -O; elsewhere its allocation moves by less than 0.1%.
Compiling !16269's ManyBasicBlocks at -O1 and -O2, the compiler
allocates 0.2% more.
Assisted-by: Claude Opus 5.5
-
f2867380
by Mikolaj Konarski at 2026-11-03T22:23:56+01:00
Hand calcFreqs a procedure's loops inner first
calcFreqs estimates block frequencies by Wu and Larus's algorithm, which
takes the loops from inner to outer, as its comment says: an enclosing
loop's frequencies use the cycle probability of each loop inside it,
computed when that loop is processed. mkGlobalWeights handed
it the loops in loopInfo's order, by back edge in the order of the CFG's
labels, so an enclosing loop could come first. calcFreqs then took
the probability of an inner loop's back edge itself, often 1.0,
for that loop's cycle probability, which calcFreqs caps at 1 - 1/512,
so the inner loop's head counted up to 512 times as often as
it was entered. The cycle a heap check makes through the garbage
collector is such an inner loop in any loop that allocates:
in containers' map benchmarks, the head of the loop that looks keys up
was estimated to run 384 times a call, while the frequencies
of the edges into it summed to 101. mkGlobalWeights now sorts the loops
by the size of their bodies, an inner loop's body being a proper subset
of an enclosing one's.
This makes block layout lean on the estimate more. Taking a procedure's
loops in label order could make a head's frequency up to 512 times
its inflow, so that the loop's back edge won by accident; where static
prediction calls staying in a loop over a tree unlikely (Note [Static
prediction of tag tests]), it is now the rule that a back edge outranks
the loop's entry ("Let a loop's back edge outrank its entry in block
layout") that keeps layout from following the estimate.
Measured against the parent commit, in instructions; calcFreqs changes
the generated code only under -fcmm-static-pred, on at -O1 and -O2.
No nofib program moves by 0.2%, no benchmark of containers or megaparsec
rises at -O2, and vector's benchmarks take 0.035% fewer at -O2
(findIndexR_manual 1.06% fewer) and fannkuch-redux 0.19% fewer.
No benchmark's allocation changes beyond nofib's run-to-run noise.
The compiler's allocation compiling perf/compiler's T3294, T12707, T783
and T9961 moves by less than 0.1%.
Assisted-by: Claude Opus 5.5
-
cfaa9bea
by Mikolaj Konarski at 2026-11-03T22:23:56+01:00
Count self loops as loops in static branch prediction
loopInfo took an edge for a back edge where its target dominates
its source, by mkDomMap's sets, which hold a block's own label only
where the block is a leaf of the dominator tree. So a self loop counted
as a back edge only from a leaf, and the retry of an atomic operation
that x86 emulates with a compare-and-swap loop (cmpxchg_code), whose
block dominates the block after it, counted as none: calcFreqs then took
that block for irreducible and gave frequency 0 to it, to every block
it dominates and to their edges, a surrounding loop's back edge among
them. mkDomMap's sets now hold each block's own label but the root's,
so that a self loop counts as a back edge but at the root, which no edge
enters as a back edge.
The retry edge cmpxchg_code adds now has a TransitionSource of its own,
AsmCodeGenRetry, which staticBranchPrediction leaves out of its rule
that a back edge is more likely than any other edge, so that the retry
is predicted to be rare rather than to repeat seven times in eight. Note
[Static Branch Prediction] says so. Andreas Klebinger's !16535 overlaps
with this commit at that edge: it builds that loop's edges
by continueInNewBlock and addColdSelfLoop, and the two changes conflict
there.
Measured against the parent commit, on x86-64: the assembly
of perf/compiler's T3294, T12707, T783 and T9961 at -O0 and -O
is the same, and so are the objects of nofib's programs at -O2;
of containers' objects only graph-benchmarks' Main changes,
and no benchmark of containers, vector or megaparsec moves beyond
its noise. mkDomMap's go is now one equation, and at -O the compiler
allocates 1.9% less compiling perf/compiler's T783, 0.1% less compiling
T12707 and 0.08% less compiling T3294; at -O0, where static prediction
is off, its allocation does not change. Compiling !16269's
ManyBasicBlocks at -O1 and -O2, the compiler allocates 10.7% less.
Assisted-by: Claude Opus 5.5
-
5dc12312
by Mikolaj Konarski at 2026-11-03T22:23:56+01:00
Specialise the linear allocator and liveness pass to x86-64
The linear allocator is overloaded in the instructions and in the free
registers, and the liveness pass in the instructions, so on x86-64 too
they called every instruction method through a dictionary: the code
generator is specialised for each target, but its calls of regAlloc
and cmmTopLiveness went to the overloaded functions. Both now have
a SPECIALISE pragma for x86's instructions, and NOINLINE [0], without
which neither rule fires, when the boot compiler builds stage 1 or when
the stage-1 compiler builds stage 2: cast worker/wrapper makes each
function an alias of its body, cast to UniqDSM, and the alias inlines
from the function's own phase whatever its rules' phase, so it inlines
into the code generator before the call there becomes one at x86's
instructions, which is what the rule rewrites. A phase on the SPECIALISE
pragmas alone, [0] or [2], makes neither rule fire. linearRegAlloc
passes the other targets' free registers through a NOINLINE function,
which the specialiser does not specialise, so that only x86-64's get
a copy of the allocator; Note [Specialising the linear allocator].
The allocator's constraint synonym no longer requires Outputable free
registers, which nothing prints, and is renamed
from OutputableRegConstraint to RegAllocConstraint. Liveness.hs now
imports GHC.CmmToAsm.X86 for its Instruction instance, which Linear.hs,
importing Liveness.hs, has as well, so the allocator's modules
that import either compile after the x86 code generator; this makes
no import cycle, as of the allocator's modules the x86 code generator
imports only GHC.CmmToAsm.Reg.Target.
Specialising joinToTargets as well, with NOINLINE [0], saved
the compiler less allocation than this commit does.
The idea of specialising the allocator comes from the draft !4764 by Ben
Gamari and Andreas Klebinger, which overlaps with this commit: !4764
specialises the linear allocator to x86-64 and i386, by INLINEABLE
pragmas on the allocator's functions and SPECIALIZE pragmas
in GHC.CmmToAsm.X86, and this commit specialises it and the liveness
pass to x86-64, by the SPECIALISE pragmas above.
The generated code is the same: nofib's object files are byte for byte
the parent's. The compiler allocates 1.7% less compiling perf/compiler's
T3294 at -O0 and 0.7% less at -O, 1.2% less compiling T12707 at -O0
and 0.4% less at -O, 1.0% less compiling T783 at -O0 and 0.2% less
at -O, and 1.7% less compiling T9961 at -O0, though less than 0.1% less
at -O, where CI compiles it. Compiling !16269's ManyBasicBlocks at -O1
and -O2, the compiler allocates 0.5% less. The compiler's code grows
by about 100 KB.
Assisted-by: Claude Opus 5.5
-
95ce447a
by Mikolaj Konarski at 2026-11-03T22:23:56+01:00
Record the linear allocator's spills only for -ddump-asm-stats
recordSpill kept every spill and reload the linear allocator inserts,
with its reason, in ra_spills, for the statistics -ddump-asm-stats
prints, their only reader. It now keeps them only when that flag is on.
The comments in GHC.CmmToAsm.Reg.Linear.Base that called the flag
-ddrop-asm-stats now call it -ddump-asm-stats. The statistics printed
are the same, and so is the generated code. The compiler allocates 0.20%
less compiling perf/compiler's T3294 at -O0.
Assisted-by: Claude Opus 5.5
-
df8bff3a
by Mikolaj Konarski at 2026-11-03T22:23:56+01:00
Check the order of liveness SCCs where jump targets are looked up
computeLiveness checked that a procedure's SCCs come in reverse
dependency order (checkIsReverseDependent) in a walk of its own,
gathering every instruction's jump destinations, and panicked otherwise.
The liveness pass looks every jump target's live-in registers up anyway,
taking none for a target missing from its map; that lookup now panics
instead, naming the target. For this, livenessSCCs enters the blocks
of a cyclic SCC in the map before the SCC's first walk, with no register
live, which is what the lookup took them to have, so that a target can
be missing only where the SCCs are out of order, or where the CFG
that gave the reachable blocks (sccBlocks) lacks it. The separate walk
goes, and with it computeLiveness, which then only called livenessSCCs:
regLiveness calls that itself. The generated code is the same.
The compiler allocates 0.6% less compiling perf/compiler's T3294 at -O0
and 0.2% less at -O, 0.5% less compiling T12707 at -O0 and 0.2% less
at -O, 0.7% less compiling T783 at -O0 and 0.2% less at -O, and 0.7%
less compiling T9961 at -O0. Compiling !16269's ManyBasicBlocks at -O1
and -O2, the compiler allocates 0.1% less.
Assisted-by: Claude Opus 5.5
-
2648f587
by Mikolaj Konarski at 2026-11-03T22:23:56+01:00
Keep a procedure's imports only where they are emitted
cmmNativeGens forced the imported symbols of each procedure, rendered
as text, to avoid a space leak, and kept them for makeImportsDoc, which
emits them only where needImportedSymbols holds; on x86-64 Linux
that takes -dynamic -fno-PIC. Where needImportedSymbols does not hold,
cmmNativeGens now neither forces nor keeps them. The generated code
is the same, also under -dynamic, -fPIC and -dynamic -fno-PIC.
The compiler allocates 0.15% less compiling perf/compiler's T9961
at -O0.
Assisted-by: Claude Opus 5.5
-
844308b3
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Copy no register list in x86's mkRU where every register counts
regUsageOfInstr on x86 and x86-64 filtered both of an instruction's
register lists, keeping the virtual registers and the real ones
an allocator may hand out (interesting), and so copied both lists
for every instruction. mkRU now returns a list unchanged where every
register in it is kept. Sylvain Henry's !16843 (GHC #28008) overlaps
with this commit: !16843's register usage DSL tests each register
as it adds it, but copies a list of registers it is given as it tests
them, where this commit returns such a list unchanged when every
register in it is kept. The generated code is the same. The compiler
allocates 1.2% less compiling perf/compiler's T3294 at -O0 and 0.5% less
at -O, 0.7% less compiling T12707 at -O0 and 0.2% less at -O, 0.5% less
compiling T783 at -O0, and 1.5% less compiling T9961 at -O0. Compiling
!16269's ManyBasicBlocks at -O1 and -O2, the compiler allocates 3.0%
less.
Assisted-by: Claude Opus 5.5
-
bfa3f4b5
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Mark the registers an instruction clobbers allocated, free or not
clobberRegs marked allocated those of the registers an instruction
clobbers that were free, listing the free registers of every class
for each instruction that writes a real register, with its own copy
of allFreeRegs from GHC.CmmToAsm.Reg.Linear.FreeRegs, and looking each
clobbered register up in the list. The FR class now has frClobberRegs,
which marks the clobbered registers allocated: by default those of them
that are free, by allFreeRegs, as clobberRegs did, since on AArch64,
RISC-V and LoongArch allocateReg panics on a register already allocated;
on x86-64, whose allocateReg marks a register that is not free
as it marks a free one, it marks every one of them, listing nothing.
clobberRegs calls it, and GHC.CmmToAsm.Reg.Linear no longer imports
the register class modules that only the copy used. The free registers
after a clobber are the same on every target, and so is the generated
code on x86-64.
The compiler allocates 0.1% less compiling perf/compiler's T3294 at -O0,
0.4% less compiling T12707, 0.5% less compiling T783, and 1.2% less
compiling T9961; at -O, 0.1% less compiling T12707. Compiling !16269's
ManyBasicBlocks at -O1 and -O2, the compiler allocates 0.4% less.
Assisted-by: Claude Opus 5.5
-
c3c51dda
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Keep an instruction's register usage in its liveness
The liveness pass's backward walk computes the register usage of every
instruction it annotates (regUsageOfInstr), and its forward walk
(livenessForward), the linear allocator's genRaInsn and the graph
allocator's spill cost (slurpSpillCostInfo) and spiller (regSpill_instr)
computed it again. Liveness now keeps it (liveUsage), and those use
the kept usage instead. patchRegsLiveInstr, which only the graph
allocator reaches, computes the patched instruction's usage again,
as an instruction's usage can depend on whether its registers are equal:
x86's VXOR of a register with itself into itself reads nothing. A new
Note in GHC.CmmToAsm.X86.Instr, [What x86's regUsageOfInstr reports],
lists that case with the others where the usage is not what
the processor does. This commit is, but for that Note, a subset
of Sylvain Henry's !16843 (GHC #28008), which keeps the usage
in the liveness annotation too and also builds it more cheaply in every
backend; !16843 was read before this commit was written. The generated
code is the same, with the linear allocator and with -fregs-graph.
The compiler allocates 2.1% less compiling perf/compiler's T3294 at -O0
and 0.8% less at -O, 1.2% less compiling T12707 at -O0 and 0.4% less
at -O, 0.9% less compiling T783 at -O0, and 1.9% less compiling T9961
at -O0. Compiling !16269's ManyBasicBlocks at -O1 and -O2, the compiler
allocates 0.1% less.
Assisted-by: Claude Opus 5.5
-
5b06e106
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Walk a block of a cyclic SCC again only when its successors change
livenessSCCs annotated the blocks of a cyclic SCC by walking all of them
again until the registers live on entry to every block stopped changing,
comparing two whole block maps as lists after each round. livenessCyclic
now walks a block again only when the registers live on entry to one
of its successors in the SCC, or their formats, have changed since
its last walk. The old loop compared only which registers are live
(the Eq of Regs ignores formats), so it could stop before a wider format
had reached every block of the SCC, the case Note [Register formats
in liveness analysis] describes; the new one carries formats through
as well. This fixes GHC #27619 and does what GHC #27437 asks: the walk
itself finds the changes, and no whole block map is compared.
This commit overlaps with Simon Jakobi's !16537, which fixes both issues
by iterating the fixpoint over the SCC's blocks until the formats
of the registers live on entry converge, and also removes the derived
Ord of formats, comparing them by width: this commit follows
it in carrying the formats through, walks again only the blocks whose
successors changed where !16537 walks every block of the SCC each round,
and takes its regression test T27619 unchanged. The derived Ord
of formats, which GHC #27619 also notes and which can keep a register
read at two vector formats at the narrower, stays, as Note [Register
formats in liveness analysis] says.
The generated code is the same on every program measured.
Of perf/compiler's T3294, T12707, T783 and T9961, only T9961 at -O has
a cyclic SCC, and its figure moves by 0.01%; compiling nofib's programs
at -O2, the compiler allocates 0.12% less in geometric mean and up
to 3.4% less (spectral/puzzle). Compiling !16269's ManyBasicBlocks
at -O1 and -O2, the compiler allocates 24.7% less.
Assisted-by: Claude Opus 5.5
-
59dc28e7
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Fill in liveBorn only for the graph allocator and -ddump-asm-liveness
The liveness pass recorded, for every instruction, the registers born
there (liveBorn), which costs a forward walk over every instruction
of every block. Only the graph allocator and -ddump-asm-liveness use it.
regLivenessBorn is now a pass of its own, which the graph allocator,
its iterative variant included, runs on its input and again after
recomputing liveness, and which CmmToAsm runs only for the dump.
The generated code is the same, with the linear allocator
and with -fregs-graph. The compiler allocates 2.4% less compiling
perf/compiler's T3294 at -O0 and 0.9% less at -O, 1.7% less compiling
T12707 at -O0 and 0.6% less at -O, 1.5% less compiling T783 at -O0
and 0.2% less at -O, and 2.0% less compiling T9961 at -O0;
with -fregs-graph it allocates up to 0.3% more (T9961 at -O0). Compiling
!16269's ManyBasicBlocks at -O1 and -O2, the compiler allocates 0.5%
less.
Assisted-by: Claude Opus 5.5
-
32cdaaff
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Prefer the register a vreg is moved into where it dies
The linear allocator preferred, for a vreg, the first register
it was recorded in on entry to any block (findPrefRealReg); a new Note,
[The first jump into a block fixes its registers], says what the first
jump into a block decides, that register included. The allocator now has
preferences, tried best first. firstFreeInOrder takes the free registers
of the vreg's class and returns the first preference among them, else
the first of them. allocRegsAndSpill_spill takes its choice, and so does
saveClobberedTemps, which took the first free register where it moves
a value out of a clobbered one. This commit adds one preference, ahead
of the first-used register: the real register the current block moves
the vreg into where the vreg dies. A value computed into a vreg
that the block then moves into R1, before an evaluation or a return,
is computed in R1, and the move, now from a register to itself,
is dropped by genRaInsn. The first-used register, which keeps a loop
variable in one register at both ends of a loop, gains nothing
for a value that dies in the block. Ranked after it, the new preference
cost nofib's scs 0.2% more instructions once later commits changed
the order of the free registers: a spilled value was reloaded
into its first-used register and then moved into R1's register.
processBlock builds, for each block, the map from a vreg to the register
the block moves it into where it dies (dyingMoves, rc_moveDests).
The preference is taken from Andreas Klebinger's draft !16693, whose
choice of a free register prefers the real register a vreg is moved
into where it dies, in any block (GHC #27742); this commit takes
the moves of the current block, a subset of those.
The map lives in a record of its own, RA_Context, which RA_State holds
in one strict field (ra_context), rather than in RA_State itself, where
an unused field alone makes the compiler allocate 0.3% more compiling
T3294 at -O0. The NCG configuration, which the allocator uses but does
not update either, moves there too (rc_config, was ra_config), and runR
takes the context in its place.
The expected assembly of codeGen/should_gen_asm's T24813 loses the movq
before each function's jmp: each popcount is now computed in R1.
This applies on every target the linear allocator serves; the figures
below are x86-64's. Measured against the parent commit, in instructions:
nofib's programs, linked with the same libraries for both, take 0.15%
fewer (0.16% at -O1, 0.58% at -O0), none as much as 0.2% more beyond
their run-to-run noise; vector's benchmarks 0.27% fewer, none more;
containers' 0.39% fewer, none as much as 0.1% more beyond their noise;
megaparsec's 0.49% fewer (skipSomeTill 1.2%, endBy1 1.2%), none more;
fannkuch-redux moves by no more than its noise. No benchmark's
allocation changes beyond nofib's run-to-run noise. The compiler
allocates 0.1% more compiling perf/compiler's T12707 at -O0, 0.2% more
compiling T783, and 0.08% more compiling T9961; compiling T3294
its allocation moves by less than 0.1%.
Assisted-by: Claude Opus 5.5
-
ba1d3ff9
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Prefer a vreg's register at a recorded jump target (GHC #27980)
firstFreeInOrder now puts first the register the vreg has on entry
to a jump target of the current block whose assignment is already
recorded (jumpLocRegs), such as a loop header jumped to from the loop's
end: in that register the vreg needs no fixup move at the jump.
For a target not yet reached, it takes the recorded assignments
of the blocks that target jumps to, up to three jumps on. The first-used
register, which a loop variable took until now, is the one the vreg
was recorded in by whichever block recorded it first, which need
not be the target. processBlock looks up the recorded assignments
of the block's jump targets once, when the block starts (rc_jumpLocs,
in RA_Context), taking the targets from a map of each block's jump
targets that linearRegAlloc' builds once for the procedure (jumpSuccs):
a target's assignment is recorded only at the block's jumps, after which
no preference is looked up. Built lazily, that list makes the compiler
allocate 0.3% more compiling perf/compiler's T3294 at -O0.
In GHC #27980's reproducer the loop's continuation then reloads
a variable straight into the register the loop header has it in,
and the fixup block goes away. vector's quickhull and nofib's sphere
and fibheaps show the same pattern, a rotation through a spill slot
gone. It is a preference, applying where the block that gives the vreg
a register jumps to a target whose assignment is recorded and the vreg's
register there is free; the fixups of GHC #27800, of which #27980
was closed as a duplicate, stay.
This applies on every target the linear allocator serves; the figures
below are x86-64's. Measured against the parent commit, in instructions:
nofib's programs, vector's benchmarks and fannkuch-redux move by no more
than their noise; containers' benchmarks take 0.01% fewer (intset
mapMonotonic 1.3%) and megaparsec's 0.005% fewer (skipManyTill 0.1%),
none as much as 0.1% more beyond their noise. No benchmark's allocation
changes beyond nofib's run-to-run noise. The compiler's allocation:
FIGURES FROM THE REBUILD.
Assisted-by: Claude Opus 5.5
-
9454d380
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Prefer the register a later block moves a vreg into where it dies
firstFreeInOrder now has a last preference: the real register a block
the vreg is live into moves it into where it dies, for a vreg computed
blocks earlier, such as R1 for a value evaluated some blocks after
it is computed (wideMoveDests, once a procedure; rc_wideDests).
The blocks that follow a failed heap or stack check, such
as those calling the garbage collector, do not count: the move there
seldom runs, and the register it names can cost a move in the hot code.
The preference is left out where that register is live together
with the vreg somewhere, which a walk of the blocks finds: at a block's
entry, after an instruction's writes, and where a vreg born while
the vreg is live is moved into that register where it dies, in the block
it is born in. The walk follows a vreg on into the vreg it is moved
into where it dies, to which the allocator gives the same register,
dropping the move; and it takes an instruction's writes
from its register usage, liveBorn being filled in only for the graph
allocator and the liveness dump. Nor is it taken where another vreg
the block reads or writes, with no location yet, has the register
as its first-used one (rc_claims): without this, in vector's leaffix
and listRank at -O1 a bound or an array reloaded after a call took
its wide destination, displacing the values whose first-used register
it is, and the loop's latch fixups grew from three instructions to five
and six. A vreg already given a location claims nothing, so
that a literal in intset instanceOrd:dense keeps its wide destination
where an already coalesced vreg has it as its first-used register.
dyingMoves now gives moves into vregs as well, and is INLINE: out
of line it makes the compiler allocate 1.0% more compiling
perf/compiler's T3294 at -O0. This preference and the earlier one
for the current block's moves ("Prefer the register a vreg is moved
into where it dies") together overlap with that of Andreas Klebinger's
draft !16693, the real register a vreg is moved into where it dies,
in any block.
Counting those blocks, in containers' map foldMap_traverseSum a node
and its field both had %rbx as their destination, the field from the hot
block that evaluates it, the node only from the heap check's failure
block, which calls the garbage collector; the node, allocated first,
took %rbx, and the field, given %rax, moved into %rbx at every
evaluation: 1.5% more instructions. CmmToAsm takes those blocks
from checkFailures (allocCold) and passes them to the linear allocator,
wherever the backend maintains the CFG, -finfo-table-map or not;
that is on x86-64 only, and elsewhere the set is empty. Leaving out
only the failure targets that no other edge enters, as an earlier form
of this change did, changes no code in perf/compiler's T3294, T12707,
T783 and T9961 or in containers' benchmarks, and makes the compiler
allocate slightly more.
The rule for a vreg born while the vreg is live keeps the register
for the later vreg, which its own block moves into it. Without it,
in containers' graph buildG the loop's index and lower bound took R2's
and R3's registers, which the block making the loop's call needed
for its arguments, and that block ended with three moves an edge, 3.3%
more instructions. Counting every vreg born while the vreg is live
and moved into the register in any block made containers' map insertWith
take 3% more instructions: its value argument arrives in R3
and is passed in R3 to both recursive calls, and the node's key, born
later in the loop and moved into R3 in another block, made it lose R3
as a preference, so every iteration moved it out of R3 and back. Note
[Frequencies before register allocation] gains a bullet saying
that where blocks give a vreg different destinations the last block's
wins, with a TODO at wideMoveDests: taking the hottest block's instead,
by mkGlobalWeights, moved containers' benchmarks both ways by up to 1.4%
and cost 0.24% more compile allocation, its estimate made
for this alone. A comment there notes that a destination no allocator
hands out, Sp, Hp or BaseReg, stays a candidate; leaving those out
changed no code measured and costs compile allocation.
Where no preference is free, firstFreeInOrder now leaves out, while
others remain, the free registers the current block moves another vreg
into where it dies: a vreg given one would keep the other from being
computed in it, or have to move out of its way. Those registers
are kept as a mask by register number, and the claims are walked only
where no earlier preference is free, so that the compiler's allocation
compiling a procedure of many C calls grows with the procedure as
at the parent. Without this, containers' graphFromEdges
and stronglyConnCompR took up to 1.1% more instructions at -O1:
the index-error block of their key lookup passes lo and hi in R3 and R4,
so the new preference gives them those registers in the lookup's loop,
and the array element, which has no preference, took the first free
register, R1's, into which the loop moves the key before storing
the element: every probe moved the element out of it first.
This applies on every target the linear allocator serves; the figures
below are x86-64's. Measured against the parent commit,
in instructions: nofib's programs take 0.09% fewer (0.07% at -O1, 0.06%
at -O0), none as much as 0.2% more beyond their run-to-run noise;
vector's benchmarks 0.26% fewer (generateIO 4%), none more; megaparsec's
0.14% fewer (skipCount 0.9%, choice 0.6%), none more; fannkuch-redux
moves by no more than its noise. The benchmarks of containers take
0.47% fewer (intset fromAscList:sparse:fusion 6.3%, intmap
symmetricDifference 5%). No benchmark's allocation changes beyond
nofib's run-to-run noise. Counting the failure blocks' moves,
the compiler allocates 0.1% more compiling perf/compiler's T3294 at -O0,
0.6% more compiling T12707, 0.7% more compiling T783, and 1.1% more
compiling T9961; leaving them out takes back half or more of each.
Compiling !16269's ManyBasicBlocks at -O1 and -O2, the compiler
allocates 0.3% more.
Some rows rise, and a later commit takes each back by undoing its cause.
At -O1 megaparsec's endBy takes 0.5% more: its loop's join fixup,
a cycle through the C stack that exists because the block the garbage
collector returns to records the join's registers, gains a move
an element; the next commit, "Allocate the blocks the garbage collector
returns to last", removes the whole fixup, and with it the same loop's
rotation in containers' map alter insert, 1.3% more here, and alterF
alter insert. containers' graph buildG takes up to 2.2% more, its loop
index, given no preference, now taking R1's register, which the call
at the loop's end needs, so that two moves an edge remain, and intmap
index 0.54% more; "Order x86-64 free registers around R1 and clobbering
writes (GHC #27981)" takes both back, returning the same block
to the shape it has at "Prefer a vreg's register at a recorded jump
target (GHC #27980)". vector's listRank takes 2.0M more instructions
at -O1; "Allocate a loop before a continuation that enters it" takes
that back: there the block before the join loads straight
into the join's registers, and the cycle through 0x40(%rsp) that holds
this commit's move is gone.
The rows of containers' intmap symmetricDifference-block, intmap
and intset union-block, map fromDescListWithKey:fusion and map
insertWith', and IntMap's and IntSet's union-disj_sn_swap
and union-disj_tn_swap at -O2, two instructions more each
by union-block's moves run once, are a trade: each pays moves in one
procedure, following from the register choices the preference makes,
with no local fix known; leaving the preference out reverts them
and gives back its gains.
Assisted-by: Claude Opus 5.5
-
9cc7edb8
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Allocate the blocks the garbage collector returns to last
The linear allocator records, at the first jump it allocates
into a block, the registers the block starts with, and later jumps
into the block move their values to match. linearRA_SCCs allocated
the SCCs in order, so the block the garbage collector returns to after
a heap or stack check fails, which jumps back into the block that made
the check, could be allocated before the hot path reached that block,
and so fix the registers it starts with. In containers' map alter
the block that builds the new node makes such a check; its return block,
allocated first, reloaded the new subtree into %rdi, R4, where
the rebalancing call takes it, while the hot path still held the other
subtree there, so the hot path reached the block through a fixup block
rotating three registers at every level the insertion returns through.
linearRA_SCCs now allocates those return blocks (the procedure's entry
blocks in the set checkFailures gives, which it now takes, those
in a cycle excepted), and any SCC only they lead to, after the others,
so that another predecessor, in map alter the hot path, records
the registers and the return block pays the fixup. As it now tests
every block for whether a jump has reached it, it looks the entry blocks
up in a set built once rather than in their list. This commit overlaps
with Sylvain Henry's !16673 (GHC #27800), read before this commit
was written: both defer, in linearRA_SCCs, return blocks outside cycles;
!16673 those that jump into a loop, calls' continuations included,
with the acyclic blocks only they lead to, and this commit
those the garbage collector returns to, whether or not they jump
into a loop, with any SCC only they lead to.
Taking no wide move destination while allocating such a block, instead,
fixes map alter but makes map insertWith take 3% more instructions,
the mirror case: its return block, allocated first, reloads the value
argument into R3's %rsi, its wide destination, where the hot path has
it; without that destination it reloads it into %rax, and the hot path
moves it out of %rsi and back at every level. Note [Frequencies before
register allocation] gains a bullet saying that checkFailures also
counts as cold blocks the hot path may run, a continuation that a call
returns to as well and a failure target that another edge enters,
and that an estimate computed in a Cmm pass could replace
the classification; linearRA_SCCs and checkFailures gain TODOs pointing
there. Where such a continuation is on the path into a loop, deferring
it measured better than leaving it out of the set: the loop
is then allocated first, starting from the block the garbage collector
returns to from the loop's own check, so that the path into the loop
pays the moves once and the loop's latches none (containers'
set-benchmarks); leaving such continuations out costs nofib's scs 1.1%
more instructions and vector's maximumOn 2.9%, and leaving out such
failure targets changes no code measured and costs compile allocation.
Allocating a loop before the path into it on purpose, by deferring
the blocks that jump into an SCC holding an entry block, costs
fannkuch-redux 13% at -O1, whose inner loop lies in its outer loop's
SCC. !16673's deferral, of every return continuation outside a cycle
that jumps into a loop, is not used either, because it was found
incorrect: it defers as cold a continuation that is itself inside a loop
through the Haskell stack, and fannkuch-redux then runs 16 to 19% more
instructions.
With the return blocks allocated last, a block that reloads a value can
find the vreg's first-used register, the one whichever block recorded
the vreg first gave it, to be another value's: in containers' map
fromList-distinctAsc at -O2 such a reload took it over its own wide
destination. firstFreeInOrder now passes over the first-used register
for the wide destination where a walk from the current block's jump
targets, through the blocks the vreg is live into and stopping
at those whose assignment is recorded, reaches no recorded block
but reaches one that moves the vreg into the wide destination where
it dies, and the value live into a block recorded with the vreg
in the first-used register reaches none of the blocks the walk passed
(staleWalk, staleMeets). The vreg is then marked (markStaleFirst),
and for a marked vreg whose walk reaches a recorded block holding
it in another free register, that register is taken: in sequence's
update.100 and adjust.100 at -O1, three continuations reloading
into the same vregs then agree, where the later two kept the stale
register and paid a swap through the C stack. And genRaInsn reloads
a vreg that an instruction only moves, where it dies, into a register
free by then straight into that register, and drops the move
(reload_into), as in map union at -O2.
Measured against the parent commit, in instructions: nofib's programs
take 0.08% fewer (wheel-sieve2 2.2%; 0.19% at -O1, fasta 10.5%), none
as much as 0.2% more beyond their run-to-run noise; vector's benchmarks
1.2% fewer (maximumOn 17%, minimumOn 15%); megaparsec's 2.8% fewer
(endBy 22%, skipMany 16%); fannkuch-redux moves by no more
than its noise. containers' benchmarks take 0.20% fewer (intset
fromList:worstCase 8.2%, and map alter insert, the case above, 1.3%).
No benchmark's allocation changes beyond nofib's run-to-run noise.
The compiler's allocation: FIGURES FROM THE REBUILD.
At -O1, vector's permutation benchmarks take 4.65% to 5.26% more,
quickhull 2.3% more and nofib's knights 0.5% more: in their loops
another predecessor now records a join's registers, one where a loop
value with no preference has taken R1's %rbx, and the predecessors
that receive an evaluated value in %rbx reach the join through a fixup
that swaps or rotates registers at every iteration. "Order x86-64 free
registers around R1 and clobbering writes (GHC #27981)", which puts %rbx
last in x86-64's fallback order, removes these moves in the same blocks;
the rotation left in quickhull's split loop is there before this commit.
The other rises are the deferral's price, and leaving the deferral out
reverts them. The blocks the garbage collector returns to now pay
the moves into the registers the hot path recorded, by design:
containers' main426 and $wascLinkTop pay 1404 and 868 instructions more
a call at -O1, and $sgo2 0.05% more; and megaparsec's many pays two
swaps through the C stack at every collection, the hot path having had
no fixup to lose. With the allocation order changed, other blocks move
values between registers, with no local fix known: containers' intset
fromAscList:sparse:fusion, map fromDescListWithKey,
fromDistinctDescList, fromAscListWithKey, fromDistinctAscList,
fromDistinctAscList:fusion and fromList-distinctAsc:fusion (-O1),
IntSet's symmetricDifference-block_sn_swap and sequence update.10,
and megaparsec's endBy1. In vector's findIndexR, which takes 3.1% more
at -O1, the procedure's entry now records the loop's registers,
with the predicate in R2's %r14, where it arrives, and another loop
value in R1's %rbx; as the loop calls the predicate through %rbx, every
iteration moves that value out of %rbx and the predicate in, by way
of %rdx: two moves more than before. "Order x86-64 free registers
around R1 and clobbering writes (GHC #27981)" puts the other value
elsewhere, removing the move out of %rbx; not deferring a return block
that a warm block enters takes back the rest, but costs nofib's fasta 13
instructions an iteration at -O1, and telling a loop's only latch
from a colder second latch needs block frequencies. At -O1 megaparsec's
skipMany takes 0.93% more, a move an element that lasts until "Keep Sp
put across loops and tag tests in stack layout" removes it: the hot
path, now allocated before the block the garbage collector returns to,
takes the first free register where the join's recorded one is expected.
Leaving the deferral out reverts it, with most of the procedures
of megaparsec's bench-speed that this commit changes; extending
the claims of "Prefer the register a later block moves a vreg into where
it dies" to the vregs a jump target writes removes the move but makes
many of containers' benchmarks take more instructions, intmap
fromAscList 3.3% and intersection-mix_nn 1.0% among them.
Assisted-by: Claude Opus 5.5
-
ba59cc76
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Allocate a loop before a continuation that enters it
The linear register allocator gives a block the assignment of the first
jump into it that it allocates, and the SCC order puts a return
continuation before a loop it jumps into. Where the continuation's jump
was the first into a block of the loop, it decided that block's
registers, and the loop's own edges into the block paid fixup moves
at every iteration: in the reproducer of GHC #27800, two swaps
an iteration.
Defer such a continuation, as the blocks the garbage collector returns
to are deferred, so that the loop's own edges decide and the fixup goes
on the continuation's edge into the loop. This takes from !16673,
by Sylvain Henry, its deferral of a return continuation that jumps
into a loop, which is what fixes the issue there, as an extension
of the series' own deferral, not as !16673's code, and adds two
conditions it lacks:
* the loop calls nothing: a loop that calls C keeps its values
in spill slots, which a deferred continuation that is hot through
the Haskell stack then pays for (nofib's fannkuch-redux ran 16% more
instructions with the deferral unconditional);
* no assignment is recorded yet for the block the continuation jumps
into: once another jump has recorded one, deferring changes only
the order of allocation, by which block layout breaks its ties
(containers' intset fromAscList, one jump more an element).
The deferral remains a bet that a continuation runs less often
than the loop it jumps into, and it loses where the continuation
is the return point of a call made at every iteration of that loop,
which then pays the fixup at every iteration instead: megaparsec's
reachOffsetNoLine takes 0.96% more instructions and reachOffset 0.36%
more, nofib's mandel2 0.33% more, and containers' fused
fromDistinctDescList rows 8.7% more at -O1. This is the trade
the deferral makes, its toggle the deferral itself, which reverts both
these and the fromAscList family's gains: the cases differ only in how
often a call path runs, which no structural order sees. A third
condition, deferring only a continuation whose call the loop it jumps
into does not reach, breaks T27800, leaving a swap through a spill slot
on both hot edges, loses every gain of containers' fromAscList family
and vector's listRank and awshcc, makes nofib's fasta take 10.3% more
at -O2, and leaves megaparsec's reachOffsetNoLine where this commit puts
it.
The test, T27800, is the Cmm of !16673's T27800.cmm, its assembly
searched for swaps and spill slots: the loop's edges carry neither,
and T27800.stderr expects the lines of the fixup on the cold edge out
of the continuation, a swap through a spill slot.
Measured against the parent commit, in instructions: vector's benchmarks
take 0.63% fewer at -O1 (hybcc 4.5%, listRank 4.4%, awshcc 4.0%, leaffix
3.4%) and move by no more than their noise at -O2; nofib's programs move
by less than 0.01% at -O2 and at -O1; containers' benchmarks move
by less than 0.01% at both levels, the fromAscList family taking 3%
to 8% fewer, its swap through a spill slot on each back edge gone,
and graphFromEdges up to 0.6% fewer at -O1; fannkuch-redux moves
by no more than its noise. No benchmark's allocation changes beyond
nofib's run-to-run noise. The compiler allocates at most 0.04% more
compiling perf/compiler's T3294, T12707, T783 and T9961.
Assisted-by: Claude Opus 5.5
-
ca25b1e3
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Skip registers written before a saved value's next read (GHC #26631)
saveClobberedTemps, saving a value out of a register an instruction
clobbers, such as an argument register of a C call, moved it
into the first free register in order, even one a later instruction
of the block writes before it reads the value again; then the value
moved again or was spilled there. It now leaves out the registers
the rest of the block writes before it next reads the value
(unwrittenBeforeRead), and where every free register is one, spills
the value at once. On System V every float and vector register
is caller-saved, so such a value live across a C call ends in memory
anyway: in simd013 two moves and a spill before the call become one
spill (GHC #26631). Writes in later blocks are not looked at.
containers' intmap and map mapMaybe and nofib's x2n1
and reverse-complement show the pattern: values crossing a C call stored
at once rather than first moved into a caller-saved register,
and mapMaybe's divisor kept out of %rdx, which cqto writes.
Spilling at once wherever the first free register is written somewhere
in the value's live range instead, by the clobber map that "Order x86-64
free registers around R1 and clobbering writes (GHC #27981)" builds
(rc_clobbers), makes fannkuch-redux take 1.1% more instructions: a loop
bound kept in %rbx across a memcpy call is spilled and reloaded at every
iteration, %rbx being written elsewhere in the bound's live range,
though not before the loop reads it.
This applies on every target the linear allocator serves; the figures
below are x86-64's. Measured against the parent commit, in instructions:
nofib's programs take 0.03% fewer (k-nucleotide 1.1%), none as much
as 0.2% more; fannkuch-redux 0.35% fewer; containers' benchmarks 0.003%
fewer, none more beyond their noise; megaparsec's 0.01% fewer (string
0.1%), none more; vector's benchmarks move by no more than their noise.
No benchmark's allocation changes beyond nofib's run-to-run noise.
The compiler's allocation compiling perf/compiler's T3294, T12707, T783
and T9961 moves by less than 0.1%.
Assisted-by: Claude Opus 5.5
-
763c908f
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Assign literals after computed values at the end of a block
GHC.Cmm.Sink emits the assignments it sinks no further at the end
of a block, in their order. An assignment of a literal to a local
register now goes after the later ones where each of those assigns,
to another local register, a literal or the result of a machine
operation that reads neither that register nor the literal's. A literal
reads neither memory nor a register, and none of the assignments
it passes reads or writes its register, so every value stays the same.
Set first, a constant can take the register that a later value is
to be in at the block's end, while the value's operands still hold
the register the constant is to be in. In Data.Text's UTF-8 decoding,
as inlined into megaparsec's benchmarks, the ASCII path sets the width,
1, and then the character, the zero-extended byte, and jumps
to the join, whose recorded assignment has the width in %r14
and the character in %rbx. Once a later commit makes %rbx x86-64's last
choice, the byte is loaded into %r14; set first, the width would
then take %rbx and the character %r14, and the join's fixup would swap
them at every character. Set last, the width takes %r14, and the byte
is zero-extended into %rbx.
A copy or an update in place keeps a literal before it: in a trial
that let it pass them as well, the literal changed the registers
of a join's recorded assignment in megaparsec's manyTill benchmark
and of a loop head's in Text.Megaparsec.Stream's reachOffset, whose
other predecessors then paid a move a character.
Measured on x86-64 with this commit placed after the register
allocator's commits, against the series without it: megaparsec's
benchmarks run 0.19% fewer instructions over those stable across runs
(someTill 1.06%, skipSomeTill 0.83%, skipSome 0.67%), none more.
Of nofib's programs only cacheprof's and k-nucleotide's objects change,
cacheprof's in the order of its instructions alone and k-nucleotide's
in its registers, and none moves by as much as 0.2%; containers'
and vector's benchmarks move by no more than their noise. The assembly
of perf/compiler's T3294, T12707, T783 and T9961 at -O0 and -O
is the same; at -O the compiler allocates up to 0.003% more compiling
them (T12707) and 0.0001% more compiling !16269's ManyBasicBlocks at -O1
and -O2; at -O0 its allocation does not change.
Metric Decrease:
CoOpt_Read
ManyAlternatives
ManyConstructors
T10421
T12425
T12707
T13035
T13056
T13253
T13379
T13960
T15164
T15304
T15630
T15630a
T15703
T18140
T18282
T18698a
T18698b
T18730
T1969
T20049
T20261
T21839c
T24471
T26425
T26989
T3064
T3294
T4801
T5030
T5321FD
T5321Fun
T5631
T6048
T783
T9198
T9233
T9630
Assisted-by: Claude Opus 5.5
-
54ee715c
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Prefer a loop variable's register for the value moved into it
A vreg that the current block moves, where it dies, into another vreg,
such as a loop's next index moved into the index variable before
the jump back to the loop's header, now prefers the register
the recorded assignment of a jump target of the block gives that other
vreg, as it prefers a real register the block moves it into:
processBlock adds such moves to rc_moveDests, from rc_jumpLocs.
In that register the move goes away, and at the jump the other vreg
is where the target's recorded assignment has it.
A move counts only where the block does not write its source again
(dyingMovesLast): a preference is looked up by the vreg, so it would
otherwise reach the vreg's later value. Where such a move counted,
in nofib's gcd, whose loop ends by shifting loop variables (t2C8 :=
t2Cb, then t2Cb := a new value), the new value took the register
the loop's header gives t2C8, and the jump to the header then had
to exchange two registers through a stack slot: 0.4% more instructions.
dyingMovesLast shares dyingMoves' test of a move where the vreg dies
(dyingMove). The moves into real registers include those of a vreg
the block writes again after the move, its later value then preferring
that register too; leaving them out changed no code measured and costs
compile time.
This applies on every target the linear allocator serves; the figures
below are x86-64's. Measured against the parent commit, in instructions:
containers' benchmarks take 0.12% fewer: map lookup absent and alterF
lookup absent 5% fewer, and lookupge-map's GE Twan 3%; fannkuch-redux
0.01% fewer; nofib's programs and vector's benchmarks move by no more
than their noise. No benchmark's allocation changes beyond nofib's
run-to-run noise. The compiler's allocation compiling perf/compiler's
T3294, T12707, T783 and T9961 moves by less than 0.1%.
Assisted-by: Claude Opus 5.5
-
5aa3ae59
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Order x86-64 free registers around R1 and clobbering writes (GHC #27981)
On x86-64, where none of its preferences is free, a vreg now takes R1's
register, %rbx, last in each of the two parts below: an evaluation
and a return write it, so a value given it may have to move out of their
way, and a loop variable given it at the loop's entry moves at every
iteration (GHC #27981). Before the others it takes the registers
no instruction writes while the vreg is live past the write: for a vreg
live across a C call, these are the registers the call preserves,
on System V R2's %r14 and %rbx, and for one still live where an argument
is moved into R2's %r14 for a jump, they exclude %r14. Given a register
such an instruction writes, the vreg would have to move out of the way.
Where no preference is free, firstFreeInOrder leaves out the free
registers the block moves other vregs into where they die,
and the register comes from a method of the FR class, frFallbackReg,
which by default takes the first register it is given; on x86-64
it is fallbackReg in GHC.CmmToAsm.Reg.Linear.X86_64, which takes
the first free register in this order, R1's register taken
from a constant rather than from globalRegMaybe at every call.
In the reproducer of GHC #27981 this commit already removes the back
edge's fixups; the next commit removes the cause the issue names.
In T27800, the test of GHC #27800, the continuation's edge into the loop
no longer swaps through a spill slot, and T27800.stderr, which expected
those lines, is deleted.
regClobbers finds, for each vreg, the registers written while
it is live, once a procedure on x86-64: the liveness pass now records,
at each instruction that writes a real register, the registers live
after it (liveAcross), and regClobbers adds the written registers
to every vreg among those that is live past the instruction, as bits
by register number, finding the vregs an instruction writes once
for the instruction rather than once for each vreg live past it;
and firstFreeInOrder, which reads a vreg's first-used register twice,
binds it strictly. Only x86-64's fallback uses them, and on the other
targets RA_Context's field is empty, so that no unread thunk keeps
the procedure's code alive. A move of the vreg itself into a register
is no such write. Leaving out the writes in the blocks that follow
a failed check costs containers' lookupge-intmap Milan lookups 0.5 to 1%
more instructions, a failure block's write having kept a vreg,
by accident, out of a register that a later vreg is moved into; Note
[Frequencies before register allocation] gains a bullet on the case,
with a TODO at regClobbers. Andreas Klebinger's draft !16693, read
before this commit was written, charges a vreg for each register
that an instruction in a loop starts writing while the vreg is live
across it, and this order overlaps with that clobber cost: both steer
a vreg away from the registers written while it is live, !16693
by a cost counted within loops, this commit by the order of the free
registers, from the writes met anywhere the vreg is live.
On x86-64, for a vreg that has preferences, all of them taken,
fallbackReg also leaves out, where a free register would remain,
the registers that the recorded assignment of a jump target of the block
gives another vreg, so that the vreg does not take a register the jump
will need for that one: a part of what GHC #26666 asks.
fallbackReg also leaves out, where others remain, the free registers
that the blocks up to three jumps ahead that the vreg is live into move
another vreg into where it dies, the vreg's own such destinations
excepted: given one, the vreg would keep the other from being computed
in it, or have to move out of its way there. Not for a vreg live across
a C call, which leaves its register at the call anyway: a block after
the call, such as k-nucleotide's copy of a new key, would put it
on an argument register that the call's own setup needs,
and the arguments were swapped through %r9. Nor where a jump target
not yet recorded that the vreg is live into has two or more
predecessors, those after a failed heap or stack check not counted:
its register there binds them all, and a walk from one of them reaching
a rare exit put megaparsec's manyTill counter on a register its hot
predecessor holds, a move in the join's fixup. With this look-ahead
IntMap's union-mix_sn_swap takes 0.42% fewer instructions
and union-mix_nn and union-mix_nn_swap 0.26% fewer at -O2, and at -O1
intmap fromAscList 0.82% fewer, fromAscList:fusion 1.11% and intset
fromAscList:sparse 0.87%. Measured at the end of the series,
the look-ahead costs the compiler 0.39% more instructions and 0.09% more
allocation compiling perf/compiler's T3294 at -O0, and 0.14% more
allocation compiling !16269's ManyBasicBlocks at -O0; in this commit's
own figures the method's saving cancels most of it.
raInsn no longer drops a move from one vreg into another, giving
the destination the source's register, where a jump target of the block
whose assignment is recorded has the destination in a free register
other than the source's and another vreg in the source's: the move
then puts the destination where the target has it, and the jump does
not have to swap it with that other vreg, as in containers' IntSet
compare (intset instanceOrd:dense). Where no target has a vreg
in the source's register, the move is dropped as before, the jump moving
the destination on its own edge rather than the move running on every
path out of the block.
The expected assembly of codeGen/should_gen_asm's
bytearray-memcpy-unroll and bytearray-memset-unroll names other
registers now; the instructions and their order are the same.
Simpler forms cost more. %rbx last for every vreg costs up to 3% more
instructions in procedures that make C calls. %r14 first as well,
wherever no vreg is live across a C call, gains in some loops but costs
up to 3% more elsewhere, where %r14 is wanted later, for R2 or by a jump
target's recorded assignment. Taking the registers a C call preserves
first for every vreg live across one, ignoring every other write, makes
fannkuch-redux take 1.3% more instructions: a loop variable live across
a call elsewhere in the procedure is given %r14 and moves out of R2's
way at every iteration.
Measured against the parent commit, in instructions: nofib's programs
take 0.38% fewer (fasta 5.1%, k-nucleotide 4.3%, gen_regexps 3.3%; 0.47%
at -O1, 0.06% at -O0), none as much as 0.2% more beyond their run-to-run
noise; fannkuch-redux 0.19% fewer; vector's benchmarks 1.0% fewer
(nextPermutationBijective 4.4%, findIndexR 3.4%), none more than 0.1%;
containers' benchmarks 0.66% fewer (set fromAscList:fusion 6.8%, intset
partition 6.1%, graph buildG 2.2%); megaparsec's benchmarks 0.59% fewer
(reachOffsetNoLine 6.7%, many 5.6%). No benchmark's allocation changes
beyond nofib's run-to-run noise. The compiler allocates 0.2% more
compiling perf/compiler's T3294 at -O0, 0.2% more compiling T12707, 0.2%
more compiling T783, and 0.3% more compiling T9961. Compiling !16269's
ManyBasicBlocks at -O1 and -O2, the compiler allocates 0.6% more.
The rises in containers are trades, each with a toggle. At -O1 IntSet's
union-common and union-mix take 0.44% to 0.85% more, union-block 0.25%
to 0.40% and IntMap's union-common 0.14% to 0.51%, and no later commit
takes them back: union_info loads the subtree field into %rdi rather
than %rsi, and each of its recursion-call blocks adds a move,
the partition of the free registers by clobbering writes being tied,
the field moved into %rsi on two paths while %rsi is written
on the third; which side wins depends on how the input sets overlap.
Not counting as clobbered a vreg's own dying-move destinations up
to three jumps ahead, except for vregs live across a C call, takes them
back, but makes intset fromAscList:sparse take 2.64% more, map
fromListWith-desc and its siblings 0.87% to 1.08% more, IntSet's
and IntMap's symmetricDifference common, mix and block rows 0.30%
to 0.59% more and Map's union-common_nn 0.49% more (-O2), and costs
0.27% more compile allocation for T3294 at -O0. map
fromListWith-desc:fusion takes 0.97% more at -O2: of four
specialisations of one insertion loop, which share R4 between two
subtree fields, the order wins three, and the variant that wins
the fourth makes the other three take 1.0% to 1.1% more. At -O1
the fused fromAscList family, intmap lookup_misses and map
fromList-distinctAsc:fusion take more by R1's place last in the order;
R1 in its natural place reverts them, but brings back graph buildG's
2.1% rise of "Prefer the register a later block moves a vreg into where
it dies" and makes intset folds:dense foldr'_maximum take 5.8% more.
intset mapMonotonic takes 1.27% more at -O2, the order putting %rsi
after %rax so that the hot path's reload cannot take %rax; leaving out
the registers jump targets give other vregs for vregs without
preferences too fixes it, but makes set alterF:insert:present take 2.4%
more, alterF:delete:absent 2.7% and map fromAscList:fusion 1.9%. intset
fromList:randomDups:fusion takes 0.55% more at -O1; R1 in its natural
place for vregs that die in their block fixes it, but gives back
folds:dense foldr'_maximum's gain (5.5%) and folds:sparse
foldr'_maximum's (0.9%).
Assisted-by: Claude Opus 5.5
-
39dfa21b
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Free every register not live into a newly reached block (GHC #27981)
On the first jump to a block, joinToTargets_first recorded, beside
the block's assignment, the block's free registers, the source block's
with those of the vregs not live into the target released, and initBlock
started the block with them. A real register still allocated at the jump
and not live into the target, R1 above all where it is allocatable,
therefore stayed allocated in the target block until the block wrote
it and that value died: a value the block reloads cannot go back to it,
and the block's jumps may need fixup moves. initBlock now derives
the free registers on entry to every block: every allocatable one
but the real registers live into it and those holding the vregs
of its recorded assignment, if it has one; a recorded assignment
no longer carries free registers, and joinToTargets no longer computes
the registers to release (to_free), nor needs FR. In its reproducer
the previous commit already removes the fixups, so the reproducer does
not show this fix alone; here its inline form goes from 14.0 to 13.0
instructions an element. allocateRealRegs marks the real registers among
those live into a block allocated, and initBlock takes the procedure's
initial free registers from RA_Context (rc_initFree) rather than rebuild
them for each block; runR starts the allocator with them too,
and with an empty block assignment, register assignment and stack map,
taking none of them as arguments; joinToTargets_first, left with two
statements, is inlined by hand at its one call site.
This applies on every target the linear allocator serves; the figures
below are x86-64's. Measured against the parent commit, in instructions:
nofib's programs, vector's benchmarks and fannkuch-redux move by no more
than their noise; containers' benchmarks take 0.03% fewer (tree edges
1.4%, graph transposeG 1.3% and buildG 1.1%), none more beyond their
noise, and megaparsec's 0.21% fewer (many 1.2%, sepEndBy 0.9%), none
more. No benchmark's allocation changes beyond nofib's run-to-run noise.
The compiler allocates 0.2% less compiling perf/compiler's T3294 at -O0,
0.8% less compiling T12707, 0.7% less compiling T783, and 1.1% less
compiling T9961. Compiling !16269's ManyBasicBlocks at -O1 and -O2,
the compiler allocates 2.5% less.
Assisted-by: Claude Opus 5.5
-
1b94a479
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Write the linear allocator's state only where it changes
releaseRegs, which genRaInsn calls twice for every instruction,
with the registers the instruction reads for the last time and
those it writes that die at once, wrote the assignment and the free
registers back to the allocator's state even where it had no register
to release; saveClobberedTemps, for every instruction that writes a real
register, folded over the whole assignment in the monad and wrote
it back even where no value sat in a clobbered register. Each write
builds a new RA_State. releaseRegs now returns at once where it has
nothing to release, and saveClobberedTemps picks the values to save
by a pure fold, in the order the monadic one took them, and writes
the state only where there are some. The generated code is the same.
The compiler allocates 2.1% less compiling perf/compiler's T3294 at -O0
and 0.8% less at -O, 1.5% less compiling T12707 at -O0 and 0.5% less
at -O, 1.4% less compiling T783 at -O0 and 0.2% less at -O, and 1.9%
less compiling T9961 at -O0, though less than 0.1% less at -O. Compiling
!16269's ManyBasicBlocks at -O1 and -O2, the compiler allocates 3.0%
less.
Assisted-by: Claude Opus 5.5
-
ce5fc4e6
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Find the linear allocator's spill candidates in one fold
Where no register of a vreg's class is free, allocRegsAndSpill_spill
frees the register of another vreg: the first one that is in a register
and in a stack slot, else the first one only in a register, among
those it need not keep. It found them by filtering the whole assignment
to the vregs in a register, deleting those to keep, listing the rest
and searching the list twice. It now folds over the assignment once,
keeping the first vreg of each kind, in the order the list had them.
The generated code is the same.
The compiler allocates 8.6% less compiling perf/compiler's T3294 at -O0
and 3.9% less at -O, and 0.5% less compiling T12707 at -O0;
its allocation compiling T783 and T9961 does not change. Compiling
!16269's ManyBasicBlocks at -O1 and -O2, the compiler allocates 0.2%
less.
Assisted-by: Claude Opus 5.5
-
2390f3fb
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Walk register assignments without listing them
clobberRegs listed the whole assignment, for every instruction
that writes a real register, to move the vregs that are in a clobbered
register and in a stack slot to the slot only; it now folds
over the assignment. joinToTargets_again listed the recorded assignment
of a jump target and the current one, to compare them ignoring
the formats; equalIgnoringFormats now compares the two maps themselves.
The generated code is the same.
The compiler allocates up to 0.07% less compiling perf/compiler's T3294,
T12707, T783 and T9961 (T9961 at -O0). Compiling !16269's
ManyBasicBlocks at -O1 and -O2, the compiler allocates 3.2% less.
Assisted-by: Claude Opus 5.5
-
9a9a919c
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Sort a procedure's blocks only where its first block is not first
regAlloc sorted the linear allocator's blocks to keep the procedure's
first block at the front; it now sorts them only where that block
is not at the front already. The generated code is the same.
The compiler allocates 0.09% less compiling perf/compiler's T3294
at -O0, 0.3% less compiling T12707 at -O0 and 0.1% less at -O, 0.3% less
compiling T783 at -O0 and 0.08% less at -O, and 0.5% less compiling
T9961 at -O0.
Assisted-by: Claude Opus 5.5
-
7d7109e3
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Compute the dying registers and the live-in map of liveness strictly
liveness1 left, in each instruction's liveness, the registers read
for the last time there and those written there that die at once
as thunks, which the linear allocator forces at every instruction;
livenessSCCs and livenessBlock likewise left the map of the registers
live into each block, and each entry, to be computed later. They are now
computed as the liveness pass goes. The generated code is the same.
The compiler allocates 0.7% less compiling perf/compiler's T3294 at -O0
and 0.3% less at -O, 0.7% less compiling T12707 at -O0 and 0.2% less
at -O, 0.8% less compiling T783 at -O0 and 0.2% less at -O, and 0.8%
less compiling T9961 at -O0.
Assisted-by: Claude Opus 5.5
-
a166f87e
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Skip the address registers x86's mkRU would drop
regUsageOfInstr on x86 and x86-64 added every register an address names
to an instruction's register lists, and mkRU then filtered out the ones
the allocators do not manage, such as Sp's %rbp, Hp's %r12 and BaseReg's
%r13, copying each list that held one: those of every access
to the stack or the heap. An address's registers that mkRU would drop
are now left out where they are added. This commit is a subset
of Sylvain Henry's !16843, whose register usage DSL adds each register,
an address's included, only where the allocators manage it.
The generated code is the same.
The compiler allocates 0.8% less compiling perf/compiler's T3294 at -O0
and 0.3% less at -O, 0.4% less compiling T12707 at -O0 and 0.1% less
at -O, 0.3% less compiling T783 at -O0, and 0.2% less compiling T9961
at -O0.
Assisted-by: Claude Opus 5.5
-
553f7428
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
List a CFG's edges once in block layout
sequenceChain listed the edges of the CFG twice, once for the chains
it builds and once for the edges left between them; it now lists them
once. The generated code is the same.
The compiler allocates 0.10% less compiling perf/compiler's T12707
at -O, 0.07% less compiling T783, and 0.03% less compiling T3294; at -O0
its allocation does not change. Compiling !16269's ManyBasicBlocks
at -O1 and -O2, the compiler allocates 0.1% less.
Assisted-by: Claude Opus 5.5
-
e7b0378a
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Build loopInfo's dominator map in one fold
loopInfo's mkDomMap listed every block's dominators as pairs, each
block's followed by those below it, and made a map of the list. It now
inserts each block into the map as it walks the dominator tree, a change
taken from Simon Jakobi's !16269 for GHC #27448; this commit is a subset
of !16269, which makes the same change to mkDomMap but for the sets,
below, and also adds the perf test ManyBasicBlocks. The sets stay
as they were, but for the root's empty one, which goes: each block
but the root maps to the blocks dominating it, itself included
and the root left out, where !16269 leaves a block out of its own set;
that part of !16269 is not taken, because it was found incorrect:
no self loop would then count as a back edge, the case the commit
counting self loops as loops fixes. loopInfo runs
under -fcmm-static-pred, on at -O1 and -O2. The generated code
is the same.
At -O the compiler allocates 0.2% less compiling perf/compiler's T3294
and T12707, 3.8% less compiling T783, and less than 0.01% less compiling
T9961; at -O0, where loopInfo does not run, its allocation moves by less
than 0.001%. Compiling !16269's ManyBasicBlocks, the compiler allocates
27.4% less at -O1 and 27.3% less at -O2.
Assisted-by: Claude Opus 5.5
-
bdca0e5e
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Build no lists of results in calcFreqs
calcFreqs, which static prediction runs for every procedure, built
a list of results for each block's out-edges (forM in calcOutFreqs) and,
for each block's predecessors, a list of Bools to test for irreducible
flow and a list of probabilities to sum. It now uses forM_, anyM
and a left fold from 0, which adds the probabilities in sum's order,
so that the frequencies are the same, and so is the generated code.
At -O the compiler allocates up to 0.10% less compiling perf/compiler's
T3294, T12707, T783 and T9961 (T12707); at -O0, where static prediction
is off, its allocation does not change. Compiling !16269's
ManyBasicBlocks at -O1 and -O2, the compiler allocates 0.3% less.
Assisted-by: Claude Opus 5.5
-
830b15d4
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Give back edges no bonus where static prediction estimates the weights
optHsPatterns raised every back edge of positive weight by backEdgeBonus
(increaseBackEdgeWeight), after a depth-first search for them;
under -fcmm-static-pred, on at -O1 and -O2, staticPredCfg then replaces
the weights of the branches its heuristics cover with its predictions
and estimates frequencies from them. The bonus is now left out there:
increaseBackEdgeWeight moves out of optHsPatterns, and optimizeCFG calls
it only where static prediction does not follow. The bonus counted only
at the branches static prediction leaves alone, a heap or stack check's
and those whose targets are all back edges, and at a two-way branch back
into the procedure's entry, whose weights static prediction starts from,
loopInfo counting no edge into the entry a back edge. Leaving it out
of those as well changed no code measured. The generated code
is the same on every program measured: perf/compiler's T3294, T12707,
T783 and T9961 at -O0 and -O, and nofib's programs at -O2 and -O1, whose
objects are identical.
At -O the compiler allocates up to 0.15% less compiling perf/compiler's
T3294, T12707, T783 and T9961 (T783); at -O0, where static prediction
is off, its allocation does not change. Compiling !16269's
ManyBasicBlocks at -O1 and -O2, the compiler allocates 0.4% less.
Assisted-by: Claude Opus 5.5
-
8259e69f
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
In sccBlocks, look blocks up in the CFG, not in a set of them
sccBlocks, in GHC.CmmToAsm.Reg.Liveness, built a set of every block
of the CFG, which holds only reachable blocks, to keep the procedure's
blocks that are in it; it now looks each block up in the CFG itself.
The generated code is the same.
The compiler allocates up to 0.24% less compiling perf/compiler's T3294,
T12707, T783 and T9961 at -O0 (T783), and up to 0.06% less at -O (T783).
Compiling !16269's ManyBasicBlocks at -O1 and -O2, the compiler
allocates 0.1% less.
Assisted-by: Claude Opus 5.5
-
67172797
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Inline x86's takeRegRegMoveInstr
takeRegRegMoveInstr, which the register allocators and the liveness pass
call on every instruction they look for a register-to-register move in,
built a Just and a pair for each move it found. Most callers take both
apart at once. Inlined, it builds neither where its caller takes them
apart. The generated code is the same.
The compiler allocates up to 0.12% less compiling perf/compiler's T3294,
T12707, T783 and T9961 at -O0 (T9961); at -O its allocation moves
by 0.01% or less.
Assisted-by: Claude Opus 5.5
-
d3389eb8
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Swap the registers of a cyclic join fixup where one instruction can
Where a jump's target has a recorded assignment, joinToTargets moves
the vregs into it, and where the moves form a cycle, handleComponent
spilled one source to its stack slot and reloaded it
into its destination after the other moves: a store and a load,
and a stack slot the vreg may not have had. The Instruction class now
has mkRegRegSwapInstr, which gives an instruction that swaps two
registers of a format where there is one, and by default Nothing; on x86
and x86-64 it is xchg for the general-purpose registers. handleComponent
now swaps a cycle's first source with its destination where it can,
so that a cycle of n moves takes n - 1 swaps; it spills as before
otherwise, as for floating-point and vector registers. An xchg is two
micro-ops on Zen 3, but breaking a cycle through a free register
instead, with three movs, ran in as many CPU cycles there and took more
instructions (megaparsec's benchmarks 0.114% more instructions at -O2,
skipManyTill 2.1% more); breaking floating-point cycles through a free
register changed nothing measured, the register cycles it would convert
occurring only a few times in all the suites. makeRegMovementGraph's
comment that cyclic components seem to occur only very rarely is gone:
with master's allocator, orthotope's $w$s$wequalT1 breaks 39 register
cycles through stack slots and its $w$s$w$ccompare1 59.
The compiler's allocation compiling perf/compiler's T3294, T12707, T783
and T9961 moves by less than 0.01% at -O0 and -O. Of nofib's programs
only puzzle moves by more than 0.2% in instructions: it runs 0.5% fewer,
and 0.7% fewer cycles. containers' sequence adjust.10, adjust.100
and adjust.1000 run 0.58%, 0.30% and 0.16% fewer instructions and intmap
fromListWith:randomDups:fusion 0.43% fewer, their cycles moving within
the noise; vector's benchmarks move by less than 0.01%, and megaparsec's
run 0.73% fewer (skipManyTill 3.9%, endBy1 2.3%), none more.
In megaparsec's count, sepBy and sepEndBy and in vector's quickhull,
cycles through stack slots become xchg instructions.
Assisted-by: Claude Opus 5.5
-
cc643248
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Store no value into the slot holding it at a join
expandNode turned a vreg that is InBoth in the same stack slot at both
ends of a join into a store into that slot, beside a register move where
the registers differ and alone where they do not, though the slot
already holds the value. Such a vreg now needs at most the move.
Measured on x86-64 with this commit placed after the stack layout
commits of an earlier form of the series, against that form,
in instructions: the change removes stores and nothing else. vector's
benchmarks take 0.27% fewer at -O2 (leaffix 5.26%, rootfix 2.82%)
and megaparsec's manyTill 1.45% to 1.49% fewer at -O2, two stores gone
from one join fixup; nofib's programs move by less than 0.2% at -O2, -O1
and -O0, and no benchmark of containers, vector or megaparsec rises
beyond its noise. The compiler's allocation compiling perf/compiler's
T3294, T12707, T783 and T9961 and !16269's ManyBasicBlocks moves
by 0.01% at most.
Assisted-by: Claude Opus 5.5
-
9680269b
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Predict branches into loop heads that only a heap or stack check makes
staticBranchPrediction's loop exit heuristic, lehPredicts, says nothing
about a branch into a loop head. A heap or stack check that fails calls
the garbage collector, which returns to a block that jumps back
to the check, and loopInfo counts that cycle as a loop; so in a loop
that holds such a check, the check's block is a loop head,
and the heuristic said nothing about the test that stays in the loop
by branching to it. loopHeads now leaves out a head whose back edges all
leave a block of coldBlocks, and the loop levels the heuristic compares
count only the loops left: counting the others too would put a block
that heads only such a cycle a level deeper than its siblings,
so that the heuristic would predict any branch into it. The set
is coldBlocks, not checkFailures: static prediction runs after register
allocation, so the cycle can close from a block the allocator put after
one that follows the failed check, which coldBlocks holds
and checkFailures does not. loopInfo no longer counts loop levels
(liLevels), which staticBranchPrediction now counts over the loops left,
and loopLevels, which had no caller, goes. A back edge leaving
a continuation that a call returns to as well as the garbage collector
counts the same, so a loop closed through such a call loses its head
too; telling the two apart moved no figure measured.
Where it changes the layout, this saves cycles, not instructions:
with the stay side predicted, the layout can let it fall through,
a taken jump fewer an iteration, and a conditional jump is one
instruction whether taken or not. Before the levels were limited,
lookupGE3's loop in containers' lookupge-map took one jump an iteration
instead of two, the tag test's jump backward in the layout to the heap
check and then one out of the comparison, and GE Twan ran 2% to 11%
fewer cycles at no more than 0.03% more instructions; limiting them
leaves that loop's jumps as they were.
Measured against the parent commit, on x86-64: the instructions
of nofib's programs and of containers', vector's and megaparsec's
benchmarks move by less than 0.01% in geometric mean. intset folds:dense
foldl'_maximum takes 6.5% fewer, the fixup block before its loop's head
now falling into the head. In cycles nofib's programs fell 0.01%
in geometric mean with the heads left out and rose 0.3% with the levels
limited, single programs moving by up to 12% either way where unchanged
code lands elsewhere: gcd's 4.7% rise comes from its bignum code,
unchanged but placed 80 bytes later, and hartel/listcopy's 12% from code
that a grown function moved. The compiler allocates 0.12% less compiling
perf/compiler's T783 at -O, 0.06% less compiling T12707, and less
than 0.01% less compiling T3294 and T9961; at -O0 its allocation moves
by less than 0.001%.
Assisted-by: Claude Opus 5.5
-
ad1c15db
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
NCG: invert floating-point conditional branches too
invertCondBranches turns a block's `jcc next; jmp other`
into `jncc other` only where the condition comes from an integer
comparison. maybeInvertCond complements the condition on the flags,
which nothing between the two jumps changes, so the swapped jumps take
the same path for every outcome of a floating-point comparison too,
an unordered one included, in each shape genCondBranch' emits for one.
Of the 173 such pairs nofib's objects keep at -O2, 166 follow a ucomisd
and go. mandel2 runs 2.6% fewer instructions and smallpt 0.7% fewer;
no other program whose code changes moves by 0.1%.
Assisted-by: Claude Opus 5.5
-
e012fcfe
by Mikolaj Konarski at 2026-11-03T22:23:57+01:00
Drop the assignments that only a discarded assignment read
Cmm sinking computes liveness once, before its walk, so where the walk
discards an assignment that nothing reads, such as a copy, it keeps
the assignments that only that one read (GHC #8327). cmmSink now reports
whether its walk discarded one that reads a register, and if it did,
dropDeadAssignments drops the assignments to local registers whose
values nothing needs, found in one pass by liveness in which
an assignment makes the registers it reads live only where its own
register is live. Nothing is folded or inlined again.
A second sinking pass over every procedure, its liveness recomputed,
drops them as well, but in a trial it made the compiler allocate 60%
more compiling perf/compiler's T3294 at -O, and its constant folding,
which in that trial rewrote (a - b) + c to a + (c - b) whatever b and c
were, cost containers' elemAt benchmarks up to 2.4% more instructions.
Recomputing liveness until no assignment is dead drops them too,
but takes a pass for each link of a chain of assignments each read only
by the next: on a procedure with a chain of 400, the compiler allocated
9.2 times what master's does.
The test, T8327, is a Cmm loop that assigns a constant read only
by a copy that nothing reads; the constant must not appear in its Cmm.
With the parent commit's compiler it does.
Measured against the parent commit, on x86-64: vector's benchmarks
at -O1 run 2.0% fewer instructions (sum[base] 14.3%, map[base] 9.1%,
spectral 4.3%, generateIO 4.0%), none more by 0.01%; in sum[base]'s
loop, the address of the SPEC token that vector's stream fusion passes
from one iteration to the next was loaded at each iteration, read only
by an assignment that sinking discarded. At -O2 none moves by more
than 0.03%, and those that move most have the same code. containers'
benchmarks at -O2 run up to 9.5% fewer (nubOrd noFusion_repeat 9.5%,
issue1202_repeat 4.7%, set alterF:insert:present 2.9%, lookupge-map GE
split 0.6%), none more; at -O1 only Data.Sequence's code changes,
and no benchmark moves beyond its noise. megaparsec's benchmarks at -O2
run up to 1.0% fewer (skipManyTill 1.0%, manyTill 0.7%), none more,
and at -O1 none moves. Of nofib's programs only rewrite's objects
change, and no program moves by as much as 0.2%; fannkuch-redux does
not change. The compiler allocates 0.03% more compiling perf/compiler's
T3294 at -O, 0.09% more compiling T12707 and T783, 0.04% more compiling
T9961, 0.1% more compiling !16269's ManyBasicBlocks at -O1 and -O2,
and 0.1% more over nofib's compilations.
Assisted-by: Claude Opus 5.5
-
cadf226e
by Mikolaj Konarski at 2026-11-03T22:23:58+01:00
Keep Sp put across loops and tag tests in stack layout
Stack layout gives a block the stack map of the first
of its predecessors it visits, and every later edge into the block gets
a fixup. It sets a diamond's frame (Note [diamond proc point]) up before
the tag test, trimmed to the variables live in the continuation,
and the continuation reloads them all, on the evaluated path too.
So in a loop whose body evaluates a variable, every iteration moves Sp
twice and stores the loop's invariants again, and where the variable
was evaluated already, it reloads registers that still hold their
values.
These changes to GHC.Cmm.LayoutStack keep Sp and the slots put:
* a loop head takes the stack map its back edges have in common,
by a second layout run where that pays, and the edge into the loop
pays the difference once (Note [Loop head stack maps]); a back edge
reached only through an unlikely branch proposes nothing, and a head
every cycle through which makes a non-tail call to its own procedure
is left alone, its loop turning about once an entry (nofib's tak,
containers' foldrWithKey');
* a loop head that takes no such map, its back edges differing
in depth or needing no fixup, gets in the second run the depth
of the deepest continuation in its loop, with the loop's invariants
in the slots that continuation's frame gives them: a call returning
into the loop keeps that depth, where it moved Sp twice
an iteration, down at the call and up at the fixup on the edge where
its return path joins a path that made no call, and the edge
into the loop pays the Sp move and the invariants' stores once (Note
[Reserved frames in loops]);
* the continuation becomes its reloads and a jump to its body, which
the evaluated path jumps to directly, where proc points
are not split and the reloads this adds at earlier proc points stay
at most two a diamond, or else for the diamonds whose tag test
follows no proc point whose reloads grow (Note [Tagged path skips
the reloads]); on a loop holding at most sixteen diamonds'
continuations the body takes every variable in its register but one
live across an inner loop that does not read it, and elsewhere only
those stored on the call's edge, reloading the rest itself where
they are used;
* the return address is stored on the call's edge only, where the call
is the only block returning to the continuation;
* a variable that at most one later call needs in a slot is stored
on the call's edge only too, that later call storing it on its own
path (Note [Tagged path skips the stores]);
* a diamond's frame keeps the stack's depth, its dead slots left
as holes (Note [Diamond frames keep their depth]);
* the frame a call into a diamond's continuation pushes is trimmed
to its live variables after layout, by a pass of its own, where,
among other conditions, proc points are not split and either
the procedure is called by a thunk of its Cmm group, as a lazy
list's producer is by the thunk of the list's tail, or the holes
are at least half the live words and the procedure is a thunk
or the continuation is on no loop: the call can evaluate a thunk
that leads back to such a frame, and each level of the recursion
would hold the holes (nofib's primes and multiplier); each call
returning there moves Sp up to the trimmed frame, the continuation
moves it back down, and the tagged path moves it nowhere (the same
Note).
The reservation leaves these cases out, each having paid more
than it saved:
* a continuation reached only through an unlikely branch, which
reserves nothing: a heap check's GC call returns into many a loop
that makes no other call, whose every entry and exit would pay
for a frame only that rare path uses (megaparsec's word32be,
containers' lookupGE);
* a loop that only permutes the variables live at its head,
as integerAdd's swap of its arguments, which turns a few times
an entry;
* a loop making a non-tail call to its own procedure, each level
of the recursion then holding the reserved frame, a stack
the garbage collector scans (containers' foldl' over a line-shaped
tree);
* a loop holding a head seeded from its back edges, whose seed expects
its variables in other slots than the reserved frame gives them
(nofib's puzzle, six register swaps at a join).
A variable that the continuation's code saves at the first call
it reaches without a conditional branch, within eight blocks, neither
reading nor redefining it on the way, is still stored before the test:
the tagged path would store it there anyway, and the call's path would
reload it at the continuation to store it again (nofib's fasta).
In a procedure that makes a non-tail call of itself, and for a diamond
on a cycle of blocks, cold ones left out, that makes no non-tail call
into the procedure's recursion (its strongly connected component
in its Cmm group's call graph, computed once a group
in GHC.Cmm.Pipeline), those stores all stay only where the call's frame,
set up there, is no deeper with them all, and otherwise each only where
that frame has its variable in the slot the diamond's frame gives it:
otherwise the slot would deepen every level of the recursion
(containers' PostOrder folds), or a loop's seeded frame would be set up
and dropped every iteration (containers' intset insert and sequence
update.10).
tagSkip, which splits the continuations, checks the limit of two reloads
a diamond first against a bound read off the graph, each diamond's
variables taken in registers times the proc points whose regions hold
its tag test, and runs its analyses only where the bound exceeds
the limit; there it computes each block's live-in and reads once for all
continuations, its loop rule (across) tests only the frame's variables,
and the continuations on one loop share each block's successors in it.
laterSaves searches only as far as each query needs in a procedure
without loops, across numbers each loop's blocks and works on bit sets,
and liftFrames and tagSkip no longer walk every frame.
In GHC.Cmm.Info.Build, cafAnal takes a jump into a split body
as a reference to its continuation's info label: otherwise each block's
set gathered every later continuation, at a cost growing with the square
of a procedure's chain of evaluations. None of this changes
the generated code.
Layout joins the block on a call's edge to the call's block where it
ends, where the call has no other way in and the procedure has a
diamond: unjoined, each tag test adds a block to the maps the later
passes keep of blocks, and at -O0, where no later pass joins blocks, a
jump.
Two tests in codeGen/should_compile check the assembly for a return
address stored below Sp, which each stored before this commit:
StackLayoutTagLoop, a loop evaluating each element in a diamond,
and StackLayoutCallLoop, a loop calling a function on one of its paths.
Taking the variables in registers at the join is a trade: containers'
set foldMap_traverseSum takes 3.77% more instructions at -O1, map's
fromSetA inner rows 0.43% to 0.45% more and intmap's fromSetA rows 0.32%
and 0.13% more, the split continuation's body moving its variables
between registers at the join where the parent's code used a memory
operand. Its toggle is Note [Tagged path skips the reloads]: the body
reading the variables from their slots instead makes nofib's compress
take 6.8% more instructions and queens 6.1% more.
Taking them in registers only on loops holding at most sixteen
continuations is a trade too: nofib's para takes 0.27% more instructions
at -O1, by one loop holding more, where taking them in registers saves
reloads and register moves at -O1 and costs reloads at -O2, nearly every
diamond there taking its call path. Lifting the limit reverts it,
but makes para take 0.50% more at -O2 and puzzle 4.38% more at -O1
and 2.23% more at -O2.
Storing on the call's edge only a variable that one later call needs
is a trade: at -O0 nofib's tak, mandel2, life, rfib, fasta and others
take more instructions, at -O1 multiplier, and at -O2 atom, ansi
and life, the path through a diamond's call paying a jump to the split
body and a register move more; the diamonds that lose and
those that gain differ only in their callers' arguments, which stack
layout cannot see. Storing every variable before the test instead makes
nofib's programs take 0.91% and 0.98% more instructions and containers'
benchmarks 2.3% and 2.7% more, at -O2 and -O1, and megaparsec's 3.4%
more at -O2.
Reserving a loop's frame is a trade: nofib's boyer2 takes 0.21% more
instructions, a loop's entry moving Sp and shuffling registers at each
iteration for a call path run less than half as often, and containers'
tree PostOrder.Foldable maximum 1.35% more at n=1000 and 0.39% more
at n=1000000; a further left-out case, for loops calling a procedure
private to the module, reverts them, but also gives back the gains
of nofib's sched (0.9%), transform (0.4%) and parstof (0.3%).
megaparsec's reachOffset-1000-1000 takes 1758 instructions
with or without that case, a few more than without the reservation:
the reservation's own trade.
The compile cost is that of these analyses: against the parent commit
the compiler allocates 2.81% more compiling perf/compiler's T12707
at -O0 and 1.63% more compiling T3294 at -O0, and 2.34% more
over nofib's compilations. Compiling !16269's ManyBasicBlocks,
the compiler's allocation: FIGURES FROM THE REBUILD.
FIGURES FROM THE REBUILD
Metric Increase:
T18304
Assisted-by: Claude Opus 5.5
-
0e9fe866
by Mikolaj Konarski at 2026-11-03T22:23:58+01:00
Jump conditionally to a likely false comparison's false target
genCondBranch jumps to the true target conditionally and to the false
one after, and ignores cml_likely. Where the layout makes either target
a fall-through, invertCondBranches and dropJumps leave one conditional
jump whichever order it emits; where neither jump falls through,
the conditional jump is the shorter way to its target,
so invertCondBranches now swaps the two jumps of an integer comparison
likely false whose true target does not come next. After layout rather
than before genCondBranch: swapped there, every heap and stack check's
jumps were swapped and swapped back by invertCondBranches, costing up
to 0.29% more instructions compiling at -O0. invert examines a block's
last two instructions once and chooses between the old rule and the new
one.
Measured on x86-64 with this commit placed at the end of an earlier form
of the series, against that end, in instructions: containers' IntMap
fromSet and fromSetA rows take 0.34% to 0.46% fewer at -O2 and fromSet
0.36% fewer at -O1, beyond the leftover jump "Drop jumps over blocks
dropJumps empties" removes there; no benchmark rises beyond its noise,
and the compiler allocates less compiling each of perf/compiler's T3294,
T12707, T783 and T9961 and !16269's ManyBasicBlocks, at every level
measured.
Assisted-by: Claude Opus 5.5
-
aa546c8c
by Mikolaj Konarski at 2026-11-03T22:23:58+01:00
Estimate block frequencies once per procedure before linear allocation
blockFrequencies in GHC.CmmToAsm.CFG makes mkGlobalWeights' estimate
of how often each block runs, for the linear allocator, from the native
CFG before allocation, under -fcmm-static-pred as the layout's estimate
is: static prediction's edge probabilities (the two-way rule factored
out of staticBranchPrediction as predictBranch, shared, given
the procedure's tagRegisters as the layout's estimate is) and calcFreqs'
fixpoint, with loops found by one depth-first search and the whole
computation in two arrays. CmmToAsm passes Linear.regAlloc the estimate
from a given entry, bound strictly so that at -O0 it is the static
noBlockFreqs and no thunk a procedure; regAlloc computes and forces it,
and the next commit uses it.
Assisted-by: Claude Opus 5.5
-
80f50ceb
by Mikolaj Konarski at 2026-11-03T22:23:58+01:00
Allocate hotter SCCs first, the procedure's first block before them
Where block frequencies are estimated, linearRA_SCCs allocates the SCCs
hotter first among those it can allocate (hotFirst, an order computed
before allocation from the jumps), the procedure's first block before
any other and another entry as hot as the hottest of itself
and the blocks it jumps to, an SCC's heat compared in factors of 1.2
with ties to the earlier in dependency order, and sets aside an entry
other than the first block while a block it jumps to, not yet reached,
runs more than 1.2 times as often as the entry; where nothing can
be allocated, the entry set aside that the estimate calls hottest goes
next. This replaces the deferral of the blocks the garbage collector
returns to and of a continuation entering a call-free loop, which stay
where there is no estimate (-O0, other targets). The first block first
keeps a loop calling itself on the argument registers (Data.Tree's
and Data.Map's folds); the factor leaves to dependency order what static
prediction's 50/50 on tag tests makes of the paths around an evaluation
(Data.IntSet's fromAscList loop); the hottest entry set aside first
gives a join of two continuations, both waiting, the hotter's registers
(Data.IntMap's mapKeysMonotonic and Data.IntSet's fromList at -O1);
an entry ranked by its hottest jump target, not above every other SCC,
keeps a continuation before the path into the block it jumps to (nofib's
fannkuch-redux at -O1) without putting colder continuations before
a hotter one (Data.IntSet's mapMonotonic at -O2). hotFirst is NOINLINE,
so that its local thunks are not allocated for every procedure at -O0,
where it is not called.
Measured on x86-64 with this commit and the previous one placed
at the end of an earlier form of the series, against that end,
in instructions: nofib's programs take 0.30% fewer at -O2, 0.29% at -O1
and 0.53% at -O0, through the libraries' code, and at -O2 and -O1
containers' benchmarks 0.39% and 0.49% fewer, vector's 0.38% and 0.73%
and megaparsec's 0.14% and 0.38%.
The order is a trade, its toggle the order itself (hotFirst's guard
in linearRA_SCCs). In loops closed through a call, which the estimate
rates at a trip multiplier far below the run's, containers' map
fromList-distinctAsc:fusion takes 2.3% more, set
fromDistinctDescList:fusion 1.9% more and intset fromRange:small 1.4%
more, and nofib's scs 0.65% more. Elsewhere containers' nubOrd
strings_noFusion_distinct takes 1.35% more, intset map:asc,
and mapMonotonic at -O1, about 1.0% to 1.3% more, intmap
fromList:asc:fusion 0.96% more, and at -O1 intset fromAscList
and fromAscList:sparse 2.7% and 2.0% more and set alterF:insert:present
0.53% more; nofib's smallpt takes 0.53% more, digits-of-e2 0.28% more
and fannkuch-redux 0.33% more at -O1.
Narrower toggles each revert one row at a cost. Making a cyclic SCC
that holds an entry not pushed at the start wait for a jump to reach
it reverts graph buildG's 0.92%, but makes intmap lookup_mixed take 5.1%
more. Letting an entry wait only while its target, not yet reached, has
another predecessor not yet allocated that runs over 1.2 times as often
as the entry reverts nofib's puzzle, 0.38% more, but gives back
containers' tree fold1 gains. Setting no entry aside reverts
megaparsec's choice, 1.8% and 1.2% more at -O2 and -O1, but makes
megaparsec's benchmarks take 3.0% and 1.3% more. Ranking every entry
above the other SCCs, rather than by its hottest jump target, reverts
map alterF's lookup present, 0.79% and 0.39% more at -O2 and -O1,
but gives back the gains of intset mapMonotonic at -O2 and
of the spanAntitone rows, and adds to scs's rise.
The estimate and the order are the compile cost: the compiler allocates
at most 0.05% more at -O0 (T9961) and 0.12% more at -O and -O2 (T12707
at -O) compiling perf/compiler's T3294, T12707, T783 and T9961,
and a stage-1 compiler runs up to 0.39% more instructions (T783 at -O).
Assisted-by: Claude Opus 5.5
-
dd425481
by Mikolaj Konarski at 2026-11-03T22:23:58+01:00
Propagate wide move destinations back, preferring the target's register
The last preference of "Prefer the register a later block moves a vreg
into where it dies", a vreg's wide destination, now reaches back along
moves: wideMoveDests also gives a vreg moved where it dies, in a warm
block, into a vreg that has a wide destination, that destination, up
to four moves back, and the walk of the blocks checks it as it checks
the others. A vreg given a destination so first takes the first-used
register of the vreg it is moved into, the claims test skipped,
as that vreg, dead, still claims its register: given the destination
itself, a closure moved into a join's vreg took the register that vreg
is moved into later, not the one it has at the join, and every path
into the join ended in a move (containers' map fromListWith-asc, 1.26%
more at -O1). Where a procedure has no candidate the propagation returns
at once, and it shares its list of moves with the walk.
In containers' tree PostOrder foldl'_sum a loop's carry is now computed
in R2's %r14, where the recursive call takes it, rather than in another
register moved there and back. Measured on x86-64 with this commit
placed at the end of an earlier form of the series, against that end,
in instructions: vector's maximumOn takes 15.4% fewer, containers' tree
PostOrder folds 4.3% to 8.1% fewer, nofib's k-nucleotide 1.67% fewer
and megaparsec's word32be and word32le about 1% fewer.
The propagation is a trade, its toggle propagating along no move, which
reverts every gain with the rises: nofib's digits-of-e2 takes 0.32%
more, a reload taking the propagated register before the join it feeds
is assigned, containers' graph bcc rows up to 0.37% more at -O2 and map
fromListWithKey-desc 0.065% more. The compiler allocates at most 0.09%
more, compiling !16269's ManyBasicBlocks at -O0.
Assisted-by: Claude Opus 5.5
-
b7fd254c
by Mikolaj Konarski at 2026-11-03T22:23:58+01:00
Give a vreg the register a recorded join wants it in
Where a register is free for a vreg but a recorded jump target's
assignment wants the vreg in another one, held by a vreg
that no instruction left in the block reads and that the recorded jump
targets hold in its slot, allocRegsAndSpill_spill now spills the holder,
a store that jump would need anyway and none where the holder is
in its slot already, and gives the vreg its register in place
of the free one. The test uses the position of each instruction
in its block and the block's last reads of the vregs live out of it,
built where the test consults them.
Measured on x86-64 with this commit placed at the end of an earlier form
of the series, against that end, in instructions: megaparsec's
benchmarks take 0.32% fewer at -O2 and 0.39% fewer at -O1 (endBy1 2.3%
to 2.6%, sepBy, sepBy1 and count about 2%) and nofib's puzzle 0.7%
fewer, no benchmark taking more. The compiler allocates 0.11% to 0.15%
more compiling at -O0.
Assisted-by: Claude Opus 5.5
-
537f45a3
by Mikolaj Konarski at 2026-11-03T22:23:58+01:00
Spill a vreg its block reads no more
Of the vregs in both a register and their slot, else of those
in a register only, allocRegsAndSpill_spill evicts the first
that no instruction left in the block reads, else the first, except
that a vreg live into a jump target with no recorded assignment
that another block jumps to as well counts as read again where neither
the block nor that target is in a cycle. Evicted, such a vreg would
be in its slot in the assignment the block records for the join,
so that every later jump there would store it and the code after
the join reload it: in megaparsec's someTill the cold paths of a UTF-8
decoder, allocated before the hot one, so cost a store and a reload
an element. Where the block or that target is in a loop, as a loop's
head is, the eviction stands: there it keeps a register free
for the loop. The victim is sought in one fold over the assignment,
and only where a vreg of the evicted register class is live out
of the block, the previous choice standing elsewhere; the map of last
reads is built only where it is consulted, and a procedure's joins
outside cycles once, where an eviction first consults them.
Measured on x86-64 with this commit and the previous one placed
at the end of an earlier form of the series, against that end,
in instructions, the rows the previous commit leaves alone: vector's
awshcc takes 1.9% fewer, tridiag 1.7% and quickhull 1.0%, containers'
IntSet and IntMap union and symmetricDifference benchmarks fall, nofib's
dom-lt takes 0.64% fewer and scs 0.45%, and megaparsec's string' 2.3%
fewer, no benchmark taking more beyond its noise. With the previous
commit the compiler allocates 0.13% more compiling perf/compiler's T3294
at -O0, and compiling !16269's ManyBasicBlocks it runs 0.5% more
instructions and allocates 0.4% more, about 0.2 and 0.3 points of them
this commit's set of a procedure's joins outside cycles.
Assisted-by: Claude Opus 5.5
-
8817aacb
by Mikolaj Konarski at 2026-11-03T22:23:58+01:00
Copy small loop heads over the jumps to them before block layout
Block layout lets one edge into a loop head fall through and makes
the others jump: the loop's entry once a run, every other back edge once
an iteration. copyLoopHeads, a new NcgImpl method that x86-64 implements
in GHC.CmmToAsm.X86.CodeGen and that cmmNativeGen runs between
optimizeCFG and sequenceTop where the chain layout follows, copies
a loop head of at most six instructions and a conditional branch
over the jumps to it, after register allocation: a block whose one
successor is the head, and which ends in a jump to it, ends
in the head's instructions instead and goes straight on to the head's
successors, whose edges it takes in the CFG, their weights shared out
in proportion to the weight of the jump it replaces. A copy
into the loop's entry saves the jump into the loop; a copy into a back
edge's source rotates the loop for that edge. Note [Copying loop heads];
Note [CFG based code layout] lists the new step.
A loop head is a target of a back edge of static prediction's loops,
which optimizeCFG now returns with the weights (mkGlobalWeightsWith
returns loopInfo's back edges too), or of findBackEdges' where static
prediction does not run. The jump that layout would let fall through
takes no copy: of the jumps to the head, the one ranked first
by the weights sequenceChain ranks edges by, a back edge raised
as preferBackEdges raises it, where it outranks every other edge in.
That jump's CFG weight becomes the raised one, since the copies can take
away the entry edges preferBackEdges raises it above, and the head's
out-edges keep their weights, so that the head keeps the loop's body
after it where static prediction rates the loop's back edge below
its entry. A block that follows a failed heap or stack check takes
no copy.
Measured against the parent commit, in instructions, with no benchmark
of the battery rising by more than its noise: at -O2 nofib's programs
take 0.20% fewer (fannkuch-redux 2.1%, knights 1.3%, eff/CSD 1.0%,
digits-of-e1 1.0%), containers' benchmarks 0.43% fewer (intmap
lookup_half 6.9%, lookup_most 5.2%, lookup_hits 5.1%, intset member
3.4%, nubInt issue1202_repeat 3.3%), megaparsec's 0.31% fewer (string'
1.2%, skipManyTill 1.0%) and vector's 1.20% fewer (nextPermutation 5.1%,
nextPermutationBijective 4.3%, hybcc 1.0%); at -O1 nofib's 0.18% fewer
(fannkuch-redux 1.9%), containers' 0.52%, megaparsec's 0.33%
(reachOffsetNoLine 1.6%, reachOffset 1.3%) and vector's 0.93% (leaffix
0.6%, awshcc 0.6%); at -O0, where it does not run, nofib's 0.12% fewer,
through the libraries' code. This takes back the trades of the commit
that lets a loop's back edge outrank its entry: IntSet's difference
and intersection benchmarks take 0.68% fewer instructions than
at the parent at -O2 and 1.17% fewer at -O1, nubInt's noFusion_repeat
2.4% fewer, megaparsec's reachOffset at -O1 1.3% fewer; and vector's
awshcc at -O1, whose loop's two back edges both jumped, executes 78M
fewer jumps a call in leaffix, the 1.5M of one of those back edges among
them. Code grows: nofib's objects' text by 0.68% at -O2 and 0.54%
at -O1, the stage-2 compiler's by 0.75%. The compiler allocates up
to 0.14% more compiling perf/compiler's T9961 at -O and 0.12% more
for T783; with no copying at -O0, up to 0.06% more, for passing the back
edges on.
Each rule that picks the jump kept took back rises of a version without
it, measured from scratch or with the stage-1 compiler: copying every
jump made the loop of two blocks in nofib's spectral-norm a block
branching to itself, 2.9% more; keeping the heaviest jump by raw weight
copied the one layout let fall through, in intmap's foldMap_elem (2.3%
more), intset's fromAscList (3.4%) and base's eqString (nofib's expert
0.26%); letting the head's out-edges lose the copies' weights gave
intset's fromAscList's body to the copy of its entry, and leaving
the kept jump's weight as it was cost nofib's mkhprog 0.55%. The bound
of six instructions: at four, nubInt's issue1202_repeat keeps a third
of its gain; at eight, no target row moved. Not taken: a block ending
in a conditional branch and a jump to the head copying it too, which
moved nothing in nofib; and a second pass, its back edges found again
by findBackEdges, which cost ManyBasicBlocks 0.5% more compile
allocation and gained no more than the rules above.
Assisted-by: Claude Opus 5.5
-
04b5a1c0
by Mikolaj Konarski at 2026-11-03T22:23:58+01:00
Walk liveness first over the instructions as the code generator gives
natCmmTopToLive wrapped every instruction as LiveInstr (Instr i)
Nothing, in a list of its own, which the first liveness walk dropped
at once for a LiveInstr carrying the liveness. Now natCmmTopToLive
returns the procedure's SCCs of NatBasicBlocks, and the walk takes
an accessor giving each element's InstrSR: Instr on the first walk,
a LiveInstr's own when the graph allocator walks again. The walk's
functions are INLINE, the recursive ones as wrappers around a local
loop, so that each form gets its own copy with the accessor known.
Measured with stage-1 compilers against the parent commit on x86-64:
the assembly of perf/compiler's T3294, T12707, T783 and T9961 at -O0, -O
and -O2 and of !16269's ManyBasicBlocks at -O0, -O1 and -O2
is byte-identical, and so is that of T3294 and T9961 at -O
under -fregs-graph and -fregs-iterative. The compiler allocates 0.89%
less compiling T3294 at -O0, 0.86% less for T9961, 0.62% for T12707
and 0.53% for T783, and 0.03 to 0.31% less at -O and -O2, but 0.04
to 0.05% more for ManyBasicBlocks, which it compiles at -O1 in 0.34%
fewer instructions under callgrind, its garbage collector copying less.
Assisted-by: Claude Opus 5.5
-
fec02739
by Mikolaj Konarski at 2026-11-03T22:23:58+01:00
Document behaviour of the NCG's CFG, layout and register allocators
Comments and documentation only, on behaviour the earlier commits do
not change; the generated code is the same.
Dead code elimination is off under -finfo-table-map (GHC #20428),
so the liveness pass gets no CFG and none is kept after register
allocation: there is no static prediction for the layout and no chain
layout under -fblock-layout-cfg. In GHC.CmmToAsm.Reg.Graph.SpillCost,
a comment says that the block frequencies scale only the counts of uses
and definitions, on which no spill choice depends.
In GHC.CmmToAsm.CFG, Note [CFG Edge Weights] gave the default weight
as 20k, where defaultWeights has 1000, and said that the false side
of a regular branch is preferred, where both sides get the same weight
and optHsPatterns' favourFewerPreds may prefer one by its predecessors,
and that calls are ignored completely, where the layout leaves a call's
edge out only while it builds chains and divides its weight by 64 when
it merges them; it now also says that under -fcmm-static-pred
the weights become estimated frequencies, for the layout and, estimated
before allocation, for the linear allocator. Note [Layout relevant edge
weights] in GHC.CmmToAsm.BlockLayout also said that calls are ignored,
and now says how the layout treats them. Comments now say that edges
of equal weight fall back on label order, which means nothing,
and that where shortcutting redirects an edge onto one already there,
one of them keeps its EdgeInfo and their weights are not summed;
in GHC.CmmToAsm.BlockLayout, that allEdges' relevantWeight differs
from directEdges'. Note [CFG based code layout] now says
that the layout's loop analysis and mkGlobalWeights run for the layout
only under -fcmm-static-pred, and the users' guide
that -fcmm-static-pred works only on x86-64, for the linear allocator's
order and, after register allocation and without -finfo-table-map,
for the layout. In X86's genCondBranch', a comment says that the 3 added
to the weight of a floating-point comparison's false edge breaks layout
ties toward the side GHC.Cmm.ContFlowOpt makes likely.
In the linear allocator, Note [Unique order in the linear allocator]
says which decisions take the order of the vregs' uniques as a ranking,
the spill victim and the order in which saveClobberedTemps moves values
out of clobbered registers, and that it orders joinToTargets' fixup
moves. The comments that said deterministic code generation
was not supported, which -fobject-determinism provides, now say, there
and in the liveness pass and the graph allocator, whether the order
matters, and in GHC.Cmm.Node, GHC.Cmm.Reg and GHC.Platform.Reg
that it is deterministic only where the uniques are. JoinToTargets'
comment on cycles through memory now says that a vreg's slot changes
where its format grows. In GHC.CmmToAsm.Reg.Liveness, the comment
on the SCCs' order says that no block may fall through before
allocation, and liveDieRead's haddock that at a branch it also holds
the registers live into its targets only.
Comments that were wrong before the series are corrected too. Step (j)
of the linear allocator's outline, re-adding a dead spilled register's
stack slot to the free list, is marked as not done, a StackMap never
freeing a slot. calcBody's TODO, that its sort would be redundant
were setElems ascending, is gone: the sort orders vertex numbers, which
follow the reverse postorder. On AArch64, getFreeRegs scans the integer
registers from x18 down, not from 28. FreeRegs' executable specification
gives noFreeRegs as [] and getFreeRegs each instance's fixed order,
where it handed out the registers most recently released first. X86's
stmtToInstrs no longer assumes the condition likely false:
GHC.Cmm.ContFlowOpt also inverts a condition whatever its likelihood
where the false target has more than one predecessor.
GHC.CmmToAsm.Reg.Linear.State cites Note [The one-shot state monad
trick] on one line, where the notes linter matches it. linearRegAlloc's
haddock and the comment over process say that a block is allocated
before a jump into it if it is any of the procedure's entry blocks,
not only its first, and that process drops a block whose assignment
never gets set, where the comment said that it would loop. Note [CFG
Edge Weights] gives likely and unlikely branches the 75% and 25%
the default weights give them, where it said more than 80% and less
than 20%.
Assisted-by: Claude Opus 5.5