Simon Jakobi pushed to branch wip/sjakobi/cbe2 at Glasgow Haskell Compiler / GHC Commits: 3968d468 by Simon Jakobi at 2026-07-05T23:29:39+02:00 CBE: never eliminate the entry block when merging duplicates The entry block's label is the procedure's externally visible symbol. CBE processes blocks in arbitrary order, so the entry block could be picked as the duplicate side of a merge and substituted away, dropping the symbol and leaving dangling references (observed with the second CBE pass on nofib's real/veritas: undefined references at link time). See the new wrinkle in Note [Second pass of CBE] in GHC.Cmm.Pipeline. Assisted-by: Claude Fable 5 - - - - - b430e823 by Simon Jakobi at 2026-07-07T13:11:40+02:00 WIP: Disable pass for measurement - - - - - 2 changed files: - compiler/GHC/Cmm/CommonBlockElim.hs - compiler/GHC/Cmm/Pipeline.hs Changes: ===================================== compiler/GHC/Cmm/CommonBlockElim.hs ===================================== @@ -71,7 +71,11 @@ elimCommonBlocksWith :: (Label -> Label -> Bool) -> CmmGraph -> CmmGraph elimCommonBlocksWith ok_to_merge g = replaceLabels env $ copyTicks env g where - env = iterate ok_to_merge mapEmpty blocks_with_key + -- The entry block must never be eliminated: its label is the + -- procedure's externally visible symbol. (It may still absorb + -- duplicates as the surviving block.) + ok_to_merge' l1 l2 = l1 /= g_entry g && ok_to_merge l1 l2 + env = iterate ok_to_merge' mapEmpty blocks_with_key -- The order of blocks doesn't matter here. While we could use -- revPostorder which drops unreachable blocks this is done in -- ContFlowOpt already which runs before this pass. So we use ===================================== compiler/GHC/Cmm/Pipeline.hs ===================================== @@ -33,6 +33,15 @@ import GHC.Platform import Control.Monad import GHC.Utils.Monad (mapAccumLM) +import System.Environment (lookupEnv) +import System.IO.Unsafe (unsafePerformIO) + +-- TEMPORARY measurement hack, not for committing: GHC_CBE2=0 disables the +-- second CBE pass, giving a same-binary A/B for size/perf comparisons. +{-# NOINLINE cbe2Enabled #-} +cbe2Enabled :: Bool +cbe2Enabled = unsafePerformIO (lookupEnv "GHC_CBE2") /= Just "0" + ----------------------------------------------------------------------------- -- | Top level driver for C-- pipeline ----------------------------------------------------------------------------- @@ -144,7 +153,7 @@ cpsTop logger platform cfg dus proc = ----------- Second common-block elimination ------------------------------ -- See Note [Second pass of CBE] (g, call_pps) <- - if cmmOptElimCommonBlks cfg && cmmOptSink cfg && not splitting_proc_points + if cbe2Enabled && cmmOptElimCommonBlks cfg && cmmOptSink cfg && not splitting_proc_points then do let stackmap_key lbl = stackMapLivenessKey platform <$> mapLookup lbl stackmaps @@ -366,6 +375,12 @@ Wrinkles: * Merging can drop return-point labels, so the call proc-point set is recomputed before CAF analysis and attachContInfoTables. +* The graph's entry block must never be the eliminated side of a merge: its + label is the procedure's externally visible symbol. Post-layout blocks are + bare enough that this genuinely happens (observed on nofib's real/veritas, + producing undefined symbol references at link time). + elimCommonBlocksWith enforces this for every CBE run. + * We don't run the pass when splitting proc points (unregisterised or non-TNTC backends): merging blocks reachable from different proc points breaks splitAtProcPoints' invariant that each block is reachable from View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/fbddce5dcddac6fdc9e669637bcf5ce... -- View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/fbddce5dcddac6fdc9e669637bcf5ce... You're receiving this email because of your account on gitlab.haskell.org.