[Git][ghc/ghc][wip/T27981-ncg-mystery-box] 76 commits: Don't report -XStrict-generated bangs under -Wredundant-bang-patterns
Mikolaj Konarski pushed to branch wip/T27981-ncg-mystery-box at Glasgow Haskell Compiler / GHC Commits: 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 - - - - - 169 changed files: - .gitmodules - + changelog.d/T27323 - changelog.d/T27722-cbe-entry-block.md - + changelog.d/T27847 - + changelog.d/T27979 - + changelog.d/T27994 - + changelog.d/caf-deadlock-27272 - + changelog.d/clc-446 - + changelog.d/clc-447 - + changelog.d/fix-nonmoving-weak-evacuation-race-23160 - + changelog.d/ghc-api-interactive-ids - compiler/GHC/Cmm/CallConv.hs - compiler/GHC/Cmm/CommonBlockElim.hs - compiler/GHC/Cmm/Info/Build.hs - compiler/GHC/Cmm/LayoutStack.hs - compiler/GHC/Cmm/Node.hs - compiler/GHC/Cmm/Opt.hs - compiler/GHC/Cmm/Pipeline.hs - compiler/GHC/Cmm/Reg.hs - compiler/GHC/Cmm/Sink.hs - compiler/GHC/CmmToAsm.hs - compiler/GHC/CmmToAsm/AArch64.hs - compiler/GHC/CmmToAsm/BlockLayout.hs - compiler/GHC/CmmToAsm/CFG.hs - compiler/GHC/CmmToAsm/Config.hs - compiler/GHC/CmmToAsm/Instr.hs - compiler/GHC/CmmToAsm/LA64.hs - compiler/GHC/CmmToAsm/Monad.hs - compiler/GHC/CmmToAsm/PPC.hs - compiler/GHC/CmmToAsm/RV64.hs - compiler/GHC/CmmToAsm/Reg/Graph.hs - compiler/GHC/CmmToAsm/Reg/Graph/Spill.hs - compiler/GHC/CmmToAsm/Reg/Graph/SpillCost.hs - compiler/GHC/CmmToAsm/Reg/Linear.hs - compiler/GHC/CmmToAsm/Reg/Linear/AArch64.hs - compiler/GHC/CmmToAsm/Reg/Linear/Base.hs - compiler/GHC/CmmToAsm/Reg/Linear/FreeRegs.hs - compiler/GHC/CmmToAsm/Reg/Linear/JoinToTargets.hs - compiler/GHC/CmmToAsm/Reg/Linear/State.hs - compiler/GHC/CmmToAsm/Reg/Linear/X86_64.hs - compiler/GHC/CmmToAsm/Reg/Liveness.hs - compiler/GHC/CmmToAsm/Reg/Regs.hs - compiler/GHC/CmmToAsm/X86.hs - compiler/GHC/CmmToAsm/X86/CodeGen.hs - compiler/GHC/CmmToAsm/X86/Instr.hs - compiler/GHC/CmmToLlvm/CodeGen.hs - compiler/GHC/CmmToLlvm/Config.hs - compiler/GHC/Core/Coercion/Axiom.hs - compiler/GHC/Core/Predicate.hs - compiler/GHC/Core/TyCo/FVs.hs - compiler/GHC/Core/TyCo/Subst.hs - compiler/GHC/Core/Type.hs - compiler/GHC/Core/Unify.hs - compiler/GHC/Data/Graph/Directed.hs - compiler/GHC/Driver/Config/CmmToAsm.hs - compiler/GHC/Driver/Config/CmmToLlvm.hs - compiler/GHC/Driver/Config/StgToCmm.hs - compiler/GHC/Driver/DynFlags.hs - compiler/GHC/Driver/Pipeline/Execute.hs - compiler/GHC/Driver/Session.hs - compiler/GHC/Hs/Decls.hs - compiler/GHC/Hs/Dump.hs - compiler/GHC/HsToCore/Match.hs - compiler/GHC/HsToCore/Match/Literal.hs - compiler/GHC/HsToCore/Monad.hs - compiler/GHC/HsToCore/Pmc.hs - compiler/GHC/HsToCore/Pmc/Desugar.hs - compiler/GHC/HsToCore/Types.hs - compiler/GHC/HsToCore/Utils.hs - compiler/GHC/Parser.y - compiler/GHC/Parser/PostProcess.hs - compiler/GHC/Platform.hs - + compiler/GHC/Platform/CpuFeatures.hs - compiler/GHC/Platform/Reg.hs - compiler/GHC/Runtime/Context.hs - compiler/GHC/Runtime/Debugger.hs - compiler/GHC/Runtime/Eval.hs - compiler/GHC/Runtime/Eval/Types.hs - compiler/GHC/Runtime/Heap/Inspect.hs - compiler/GHC/StgToCmm/Config.hs - compiler/GHC/StgToCmm/Prim.hs - compiler/GHC/SysTools/Cpp.hs - compiler/GHC/Tc/Instance/Family.hs - compiler/GHC/Tc/Solver/FunDeps.hs - compiler/GHC/Tc/Utils/Unify.hs - compiler/GHC/Types/RepType.hs - compiler/GHC/Types/Var/Env.hs - compiler/ghc.cabal.in - docs/users_guide/using-optimisation.rst - ghc/GHCi/UI.hs - ghc/GHCi/UI/Monad.hs - libraries/base/changelog.md - libraries/ghc-internal/src/GHC/Internal/Control/Exception/Base.hs - libraries/ghc-internal/src/GHC/Internal/IO/Handle/Internals.hs - libraries/ghc-internal/src/GHC/Internal/IO/Handle/Text.hs - libraries/ghc-internal/src/GHC/Internal/IO/Handle/Windows.hs - libraries/ghc-internal/src/GHC/Internal/System/Environment/ExecutablePath.hsc - libraries/ghc-internal/src/GHC/Internal/System/IO/Error.hs - rts/StgStdThunks.cmm - rts/Weak.c - rts/sm/Evac.c - + testsuite/tests/cmm/should_compile/T27800.cmm - + testsuite/tests/cmm/should_compile/T8327.cmm - testsuite/tests/cmm/should_compile/all.T - + testsuite/tests/cmm/should_compile/cmm_fold_elemAt.hs - + testsuite/tests/cmm/should_compile/cmm_fold_lit.cmm - + testsuite/tests/cmm/should_run/T27722.hs - + testsuite/tests/cmm/should_run/T27722_cmm.cmm - testsuite/tests/cmm/should_run/all.T - + testsuite/tests/codeGen/should_compile/StackLayoutCallLoop.hs - + testsuite/tests/codeGen/should_compile/StackLayoutTagLoop.hs - + testsuite/tests/codeGen/should_compile/T27979.hs - + testsuite/tests/codeGen/should_compile/T27979.stderr - testsuite/tests/codeGen/should_compile/all.T - testsuite/tests/codeGen/should_gen_asm/T24813.asm - testsuite/tests/codeGen/should_gen_asm/bytearray-memcpy-unroll.asm - testsuite/tests/codeGen/should_gen_asm/bytearray-memset-unroll.asm - testsuite/tests/count-deps/CountDepsParser.stdout - + testsuite/tests/exceptions/T27851.hs - + testsuite/tests/exceptions/T27851.stderr - + testsuite/tests/exceptions/T27851b.hs - + testsuite/tests/exceptions/T27851b.stderr - + testsuite/tests/exceptions/T27851c.hs - + testsuite/tests/exceptions/T27851c.stdout - + testsuite/tests/exceptions/T27851d.hs - + testsuite/tests/exceptions/T27851d.stdout - + testsuite/tests/exceptions/T27851d.stdout-javascript-unknown-ghcjs - testsuite/tests/exceptions/all.T - testsuite/tests/ghc-api/exactprint/Test20239.stderr - testsuite/tests/ghc-e/should_fail/T18441fail7.stderr - testsuite/tests/ghc-e/should_run/ghc-e005.stderr - + testsuite/tests/ghci.debugger/scripts/RttiSharedTyVar.hs - + testsuite/tests/ghci.debugger/scripts/RttiSharedTyVar.script - + testsuite/tests/ghci.debugger/scripts/RttiSharedTyVar.stdout - testsuite/tests/ghci.debugger/scripts/all.T - + testsuite/tests/haddock/haddock_testsuite/IncrementalUnitId.hs - testsuite/tests/haddock/haddock_testsuite/Makefile - testsuite/tests/haddock/haddock_testsuite/all.T - + testsuite/tests/haddock/haddock_testsuite/haddockIncrementalUnitId.stdout - testsuite/tests/haddock/should_compile_flag_haddock/T17544.stderr - testsuite/tests/haddock/should_compile_flag_haddock/T17544_kw.stderr - testsuite/tests/haddock/should_compile_flag_haddock/T24221.stderr - testsuite/tests/parser/should_compile/DumpParsedAst.stderr - testsuite/tests/parser/should_compile/DumpRenamedAst.stderr - testsuite/tests/parser/should_compile/T14189.stderr - testsuite/tests/parser/should_compile/T15323.stderr - testsuite/tests/parser/should_compile/T20452.stderr - + testsuite/tests/pmcheck/should_compile/T27323.hs - + testsuite/tests/pmcheck/should_compile/T27323b.hs - testsuite/tests/pmcheck/should_compile/all.T - testsuite/tests/printer/T18791.stderr - + testsuite/tests/regalloc/T27619.hs - testsuite/tests/regalloc/all.T - + testsuite/tests/rts/T23160.hs - + testsuite/tests/rts/T23160.stdout - + testsuite/tests/rts/T27272.hs - + testsuite/tests/rts/T27272_gen.py - testsuite/tests/rts/all.T - + testsuite/tests/typecheck/should_compile/T27994.hs - + testsuite/tests/typecheck/should_compile/T27994a.hs - + testsuite/tests/typecheck/should_compile/T27994b.hs - testsuite/tests/typecheck/should_compile/all.T - testsuite/tests/warnings/should_compile/Overflow.stderr-ws-32 - testsuite/tests/warnings/should_compile/Overflow.stderr-ws-64 - testsuite/tests/warnings/should_compile/T13256.stderr-ws-32 - testsuite/tests/warnings/should_compile/T13256.stderr-ws-64 - utils/check-exact/ExactPrint.hs - utils/haddock/CHANGES.md - utils/haddock/haddock-api/src/Haddock/Interface.hs The diff was not included because it is too large. View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/72f1199ebeab1697a86af1524a16719... -- View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/72f1199ebeab1697a86af1524a16719... You're receiving this email because of your account on gitlab.haskell.org. Manage all notifications: https://gitlab.haskell.org/-/profile/notifications | Help: https://gitlab.haskell.org/help
participants (1)
-
Mikolaj Konarski (@Mikolaj)