Simon Jakobi pushed to branch wip/sjakobi/T27619-liveness-fixpoint-formats at Glasgow Haskell Compiler / GHC
Commits:
-
103d8ba4
by Simon Jakobi at 2026-08-17T11:02:25+02:00
-
8d5058d9
by Simon Jakobi at 2026-08-17T11:02:25+02:00
7 changed files:
- compiler/GHC/CmmToAsm/Format.hs
- compiler/GHC/CmmToAsm/PPC/CodeGen.hs
- compiler/GHC/CmmToAsm/Reg/Liveness.hs
- compiler/GHC/CmmToAsm/Reg/Regs.hs
- compiler/GHC/Types/Unique/FM.hs
- + testsuite/tests/regalloc/T27619.hs
- testsuite/tests/regalloc/all.T
Changes:
| ... | ... | @@ -23,6 +23,7 @@ module GHC.CmmToAsm.Format ( |
| 23 | 23 | vecFormat,
|
| 24 | 24 | isVecFormat,
|
| 25 | 25 | cmmTypeFormat,
|
| 26 | + compareFormat,
|
|
| 26 | 27 | formatToWidth,
|
| 27 | 28 | scalarWidth,
|
| 28 | 29 | formatInBytes,
|
| ... | ... | @@ -40,11 +41,13 @@ where |
| 40 | 41 | |
| 41 | 42 | import GHC.Prelude
|
| 42 | 43 | |
| 44 | +import Data.Semigroup ( (<>) )
|
|
| 45 | + |
|
| 43 | 46 | import GHC.Cmm
|
| 44 | 47 | import GHC.Platform.Reg ( Reg(..), RealReg, VirtualReg )
|
| 45 | 48 | import GHC.Types.Unique ( Uniquable(..) )
|
| 46 | 49 | import GHC.Types.Unique.Set
|
| 47 | -import GHC.Utils.Outputable
|
|
| 50 | +import GHC.Utils.Outputable hiding ( (<>) )
|
|
| 48 | 51 | import GHC.Utils.Panic
|
| 49 | 52 | |
| 50 | 53 | {- Note [GHC's data format representations]
|
| ... | ... | @@ -92,7 +95,9 @@ data Format |
| 92 | 95 | | FF64
|
| 93 | 96 | | VecFormat !Length -- ^ number of elements (always at least 2)
|
| 94 | 97 | !ScalarFormat -- ^ format of each element
|
| 95 | - deriving (Show, Eq, Ord)
|
|
| 98 | + deriving (Show, Eq)
|
|
| 99 | + -- No Ord: compare via 'formatToWidth', or use 'compareFormat' where a
|
|
| 100 | + -- total order is needed.
|
|
| 96 | 101 | |
| 97 | 102 | pattern IntegerFormat :: Format
|
| 98 | 103 | pattern IntegerFormat <- ( isIntegerFormat -> True )
|
| ... | ... | @@ -117,7 +122,7 @@ data ScalarFormat |
| 117 | 122 | | FmtInt64
|
| 118 | 123 | | FmtFloat
|
| 119 | 124 | | FmtDouble
|
| 120 | - deriving (Show, Eq, Ord)
|
|
| 125 | + deriving (Show, Eq)
|
|
| 121 | 126 | |
| 122 | 127 | scalarFormatFormat :: ScalarFormat -> Format
|
| 123 | 128 | scalarFormatFormat = \case
|
| ... | ... | @@ -248,6 +253,33 @@ scalarWidth = \case |
| 248 | 253 | formatInBytes :: Format -> Int
|
| 249 | 254 | formatInBytes = widthInBytes . formatToWidth
|
| 250 | 255 | |
| 256 | +-- | Total order on formats: by width, with an arbitrary but fixed tiebreak
|
|
| 257 | +-- between distinct formats of the same width.
|
|
| 258 | +--
|
|
| 259 | +-- See Note [Convergence of the liveness fixpoint] in GHC.CmmToAsm.Reg.Liveness.
|
|
| 260 | +compareFormat :: Format -> Format -> Ordering
|
|
| 261 | +compareFormat f1 f2 =
|
|
| 262 | + compare (formatToWidth f1) (formatToWidth f2) <> compare (tag f1) (tag f2)
|
|
| 263 | + where
|
|
| 264 | + tag :: Format -> (Int, Length)
|
|
| 265 | + tag = \case
|
|
| 266 | + II8 -> (0, 0)
|
|
| 267 | + II16 -> (1, 0)
|
|
| 268 | + II32 -> (2, 0)
|
|
| 269 | + II64 -> (3, 0)
|
|
| 270 | + FF32 -> (4, 0)
|
|
| 271 | + FF64 -> (5, 0)
|
|
| 272 | + VecFormat l s -> (6 + scalarTag s, l)
|
|
| 273 | + |
|
| 274 | + scalarTag :: ScalarFormat -> Int
|
|
| 275 | + scalarTag = \case
|
|
| 276 | + FmtInt8 -> 0
|
|
| 277 | + FmtInt16 -> 1
|
|
| 278 | + FmtInt32 -> 2
|
|
| 279 | + FmtInt64 -> 3
|
|
| 280 | + FmtFloat -> 4
|
|
| 281 | + FmtDouble -> 5
|
|
| 282 | + |
|
| 251 | 283 | --------------------------------------------------------------------------------
|
| 252 | 284 | |
| 253 | 285 | -- | A typed virtual register: a virtual register, together with the specific
|
| ... | ... | @@ -484,7 +484,7 @@ getRegister' _ _ (CmmMachOp (MO_SS_Conv src tgt) [CmmLoad mem pk _]) |
| 484 | 484 | , src < tgt = do
|
| 485 | 485 | let format = cmmTypeFormat pk
|
| 486 | 486 | -- lwa is DS-form. See Note [Power instruction format]
|
| 487 | - let form = if format >= II32 then DS else D
|
|
| 487 | + let form = if formatToWidth format >= W32 then DS else D
|
|
| 488 | 488 | Amode addr addr_code <- getAmode form mem
|
| 489 | 489 | let code dst = assert (format == intFormat src)
|
| 490 | 490 | $ addr_code `snocOL` LA format dst addr
|
| ... | ... | @@ -879,7 +879,7 @@ computeLiveness platform sccs |
| 879 | 879 | , ppr sccs'])
|
| 880 | 880 | |
| 881 | 881 | livenessSCCs
|
| 882 | - :: Instruction instr
|
|
| 882 | + :: forall instr. Instruction instr
|
|
| 883 | 883 | => Platform
|
| 884 | 884 | -> BlockMap Regs
|
| 885 | 885 | -> [SCC (LiveBasicBlock instr)] -- accum
|
| ... | ... | @@ -897,37 +897,50 @@ livenessSCCs platform blockmap done (AcyclicSCC block : sccs) |
| 897 | 897 | livenessSCCs platform blockmap done
|
| 898 | 898 | (CyclicSCC blocks : sccs) =
|
| 899 | 899 | livenessSCCs platform blockmap' (CyclicSCC blocks':done) sccs
|
| 900 | - where (blockmap', blocks')
|
|
| 901 | - = iterateUntilUnchanged linearLiveness equalBlockMaps
|
|
| 902 | - blockmap blocks
|
|
| 900 | + where (blockmap', blocks') = fixpoint blockmap
|
|
| 903 | 901 | |
| 904 | - iterateUntilUnchanged
|
|
| 905 | - :: (a -> b -> (a,c)) -> (a -> a -> Bool)
|
|
| 906 | - -> a -> b
|
|
| 907 | - -> (a,c)
|
|
| 908 | - |
|
| 909 | - iterateUntilUnchanged f eq aa b = go aa
|
|
| 902 | + -- See Note [Convergence of the liveness fixpoint]
|
|
| 903 | + fixpoint :: BlockMap Regs -> (BlockMap Regs, [LiveBasicBlock instr])
|
|
| 904 | + fixpoint bm
|
|
| 905 | + | all unchanged blocks = (bm', blocks'')
|
|
| 906 | + | otherwise = fixpoint bm'
|
|
| 910 | 907 | where
|
| 911 | - go a = if eq a a' then ac else go a'
|
|
| 912 | - where
|
|
| 913 | - ac@(a', _) = f a b
|
|
| 914 | - |
|
| 915 | - linearLiveness
|
|
| 916 | - :: Instruction instr
|
|
| 917 | - => BlockMap Regs -> [LiveBasicBlock instr]
|
|
| 918 | - -> (BlockMap Regs, [LiveBasicBlock instr])
|
|
| 919 | - |
|
| 920 | - linearLiveness = mapAccumL (livenessBlock platform)
|
|
| 921 | - |
|
| 922 | - -- probably the least efficient way to compare two
|
|
| 923 | - -- BlockMaps for equality.
|
|
| 924 | - equalBlockMaps :: BlockMap Regs -> BlockMap Regs -> Bool
|
|
| 925 | - equalBlockMaps a b
|
|
| 926 | - = a' == b'
|
|
| 927 | - where a' = mapToList a
|
|
| 928 | - b' = mapToList b
|
|
| 929 | - -- See Note [Unique Determinism and code generation]
|
|
| 930 | - |
|
| 908 | + (bm', blocks'') = mapAccumL (livenessBlock platform) bm blocks
|
|
| 909 | + |
|
| 910 | + unchanged :: LiveBasicBlock instr -> Bool
|
|
| 911 | + unchanged block =
|
|
| 912 | + case (mapLookup bid bm, mapLookup bid bm') of
|
|
| 913 | + (Just old, Just new) -> old `equalRegs` new
|
|
| 914 | + (Nothing, _ ) -> False -- first iteration
|
|
| 915 | + (Just _, Nothing ) -> False -- can't happen
|
|
| 916 | + where bid = blockId block
|
|
| 917 | + |
|
| 918 | +{- Note [Convergence of the liveness fixpoint]
|
|
| 919 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
| 920 | +For a cyclic SCC, we iterate 'livenessBlock' over the SCC's blocks until the
|
|
| 921 | +recorded entry liveness stops changing. Two subtleties in the convergence
|
|
| 922 | +test:
|
|
| 923 | + |
|
| 924 | +* It must compare register formats, not just sets of live registers –
|
|
| 925 | + hence the format-aware 'equalRegs'. A block's live-in set joins the
|
|
| 926 | + successors' entries with 'unionRegsMaxFmt', so a wide format may take one
|
|
| 927 | + iteration per control-flow edge to propagate backwards around a loop. The
|
|
| 928 | + register sets themselves are typically complete after the first iteration.
|
|
| 929 | + If we stopped as soon as the sets stabilise, a register could be recorded
|
|
| 930 | + at a narrower format than the reads it flows into. That would violate
|
|
| 931 | + property (FmtBwd1) of Note [Register formats in liveness analysis], which
|
|
| 932 | + the register allocator relies on for spill and reload widths. See #27619.
|
|
| 933 | + |
|
| 934 | +* It is sufficient to compare the entries of the SCC's own blocks. No other
|
|
| 935 | + entries can change: 'livenessBlock' inserts only the block it processes,
|
|
| 936 | + and successor SCCs are already final because SCCs are processed in reverse
|
|
| 937 | + dependency order. Comparing the whole accumulated block map would make the
|
|
| 938 | + fixpoint quadratic in procedure size (#27437).
|
|
| 939 | + |
|
| 940 | +To ensure termination, 'maxRegWithFormat' uses 'compareFormat' which defines a
|
|
| 941 | +total order on formats. Comparing the widths only would introduce the risk of
|
|
| 942 | +an infinite loop where each iteration swaps two formats of the same width.
|
|
| 943 | +-}
|
|
| 931 | 944 | |
| 932 | 945 | |
| 933 | 946 | -- | Annotate a basic block with register liveness information.
|
| ... | ... | @@ -13,16 +13,19 @@ module GHC.CmmToAsm.Reg.Regs ( |
| 13 | 13 | shrinkingRegs,
|
| 14 | 14 | mapRegs,
|
| 15 | 15 | elemRegs, lookupReg,
|
| 16 | + equalRegs,
|
|
| 16 | 17 | |
| 17 | 18 | ) where
|
| 18 | 19 | |
| 19 | 20 | import GHC.Prelude
|
| 20 | 21 | |
| 21 | 22 | import GHC.Platform.Reg ( Reg )
|
| 22 | -import GHC.CmmToAsm.Format ( Format, RegWithFormat(..), isVecFormat )
|
|
| 23 | +import GHC.CmmToAsm.Format ( Format, RegWithFormat(..), isVecFormat,
|
|
| 24 | + compareFormat, formatToWidth )
|
|
| 23 | 25 | |
| 24 | 26 | import GHC.Utils.Outputable ( Outputable )
|
| 25 | 27 | import GHC.Types.Unique ( Uniquable(..) )
|
| 28 | +import GHC.Types.Unique.FM ( equalUFMBy )
|
|
| 26 | 29 | import GHC.Types.Unique.Set
|
| 27 | 30 | |
| 28 | 31 | import Data.Coerce ( coerce )
|
| ... | ... | @@ -33,15 +36,16 @@ import Data.Coerce ( coerce ) |
| 33 | 36 | -- register liveness analysis. See Note [Register formats in liveness analysis]
|
| 34 | 37 | -- in GHC.CmmToAsm.Reg.Liveness.
|
| 35 | 38 | newtype Regs = Regs { getRegs :: UniqSet RegWithFormat }
|
| 36 | - deriving newtype (Eq, Outputable)
|
|
| 39 | + deriving newtype (Outputable)
|
|
| 37 | 40 | |
| 38 | 41 | maxRegWithFormat :: RegWithFormat -> RegWithFormat -> RegWithFormat
|
| 39 | 42 | maxRegWithFormat r1@(RegWithFormat _ fmt1) r2@(RegWithFormat _ fmt2)
|
| 40 | - = if fmt1 >= fmt2
|
|
| 41 | - then r1
|
|
| 42 | - else r2
|
|
| 43 | - -- Re-using one of the arguments avoids allocating a new 'RegWithFormat',
|
|
| 44 | - -- compared with returning 'RegWithFormat r1 (max fmt1 fmt2)'.
|
|
| 43 | + | LT <- compareFormat fmt1 fmt2 = r2
|
|
| 44 | + | otherwise = r1
|
|
| 45 | + -- See Note [Convergence of the liveness fixpoint] in
|
|
| 46 | + -- GHC.CmmToAsm.Reg.Liveness.
|
|
| 47 | + --
|
|
| 48 | + -- Re-using one of the arguments avoids allocating a new 'RegWithFormat'.
|
|
| 45 | 49 | |
| 46 | 50 | noRegs :: Regs
|
| 47 | 51 | noRegs = Regs emptyUniqSet
|
| ... | ... | @@ -64,7 +68,7 @@ minusCoveredRegs = coerce $ minusUniqSet_C f |
| 64 | 68 | where
|
| 65 | 69 | f :: RegWithFormat -> RegWithFormat -> Maybe RegWithFormat
|
| 66 | 70 | f r1@(RegWithFormat _ fmt1) (RegWithFormat _ fmt2) =
|
| 67 | - if fmt2 >= fmt1
|
|
| 71 | + if formatToWidth fmt2 >= formatToWidth fmt1
|
|
| 68 | 72 | ||
|
| 69 | 73 | not ( isVecFormat fmt1 )
|
| 70 | 74 | -- See Wrinkle [Don't allow scalar partial writes]
|
| ... | ... | @@ -97,7 +101,7 @@ shrinkingRegs = coerce $ minusUniqSet_C f |
| 97 | 101 | where
|
| 98 | 102 | f :: RegWithFormat -> RegWithFormat -> Maybe RegWithFormat
|
| 99 | 103 | f (RegWithFormat _ fmt1) r2@(RegWithFormat _ fmt2)
|
| 100 | - | fmt2 < fmt1
|
|
| 104 | + | formatToWidth fmt2 < formatToWidth fmt1
|
|
| 101 | 105 | = Just r2
|
| 102 | 106 | | otherwise
|
| 103 | 107 | = Nothing
|
| ... | ... | @@ -117,3 +121,10 @@ elemRegs r (Regs live) = elemUniqSet_Directly (getUnique r) live |
| 117 | 121 | lookupReg :: Reg -> Regs -> Maybe Format
|
| 118 | 122 | lookupReg r (Regs live) =
|
| 119 | 123 | regWithFormat_format <$> lookupUniqSet_Directly live (getUnique r)
|
| 124 | + |
|
| 125 | +-- | Do the two sets contain the same registers, at the same formats?
|
|
| 126 | +equalRegs :: Regs -> Regs -> Bool
|
|
| 127 | +equalRegs (Regs a) (Regs b) = equalUFMBy sameFormat (getUniqSet a) (getUniqSet b)
|
|
| 128 | + where
|
|
| 129 | + -- Registers with equal uniques are equal, so only compare the formats.
|
|
| 130 | + sameFormat (RegWithFormat _ fmt1) (RegWithFormat _ fmt2) = fmt1 == fmt2 |
| ... | ... | @@ -67,6 +67,7 @@ module GHC.Types.Unique.FM ( |
| 67 | 67 | strictIntersectUFM_C,
|
| 68 | 68 | disjointUFM,
|
| 69 | 69 | equalKeysUFM,
|
| 70 | + equalUFMBy,
|
|
| 70 | 71 | diffUFM,
|
| 71 | 72 | nonDetStrictFoldUFM, nonDetFoldUFM, nonDetStrictFoldUFM_DirectlyM,
|
| 72 | 73 | nonDetFoldWithKeyUFM,
|
| ... | ... | @@ -590,7 +591,12 @@ unsafeCastUFMKey (UFM m) = UFM m |
| 590 | 591 | |
| 591 | 592 | -- Determines whether two 'UniqFM's contain the same keys.
|
| 592 | 593 | equalKeysUFM :: UniqFM key a -> UniqFM key b -> Bool
|
| 593 | -equalKeysUFM (UFM m1) (UFM m2) = liftEq (\_ _ -> True) m1 m2
|
|
| 594 | +equalKeysUFM = equalUFMBy (\_ _ -> True)
|
|
| 595 | + |
|
| 596 | +-- | Determines whether two 'UniqFM's contain the same keys, with values
|
|
| 597 | +-- that agree according to the given predicate.
|
|
| 598 | +equalUFMBy :: (a -> b -> Bool) -> UniqFM key a -> UniqFM key b -> Bool
|
|
| 599 | +equalUFMBy eq (UFM m1) (UFM m2) = liftEq eq m1 m2
|
|
| 594 | 600 | |
| 595 | 601 | -- | An edit on type @a@, relating an element of a container (like an entry in a
|
| 596 | 602 | -- map or a line in a file) before and after.
|
| 1 | +{-# LANGUAGE MagicHash, UnboxedTuples #-}
|
|
| 2 | + |
|
| 3 | +-- The native code for this loop is a three-block cycle H -> X -> W -> H:
|
|
| 4 | +--
|
|
| 5 | +-- H, X: read v at FF64 (lane-0 extracts)
|
|
| 6 | +-- W: reads v at F64x2 (the full unpack)
|
|
| 7 | +--
|
|
| 8 | +-- v is loop-invariant, so its live format on entry to every block in the
|
|
| 9 | +-- cycle must be F64x2. -fno-cse keeps the two syntactically identical
|
|
| 10 | +-- lane-0 extracts from being merged.
|
|
| 11 | +module T27619 where
|
|
| 12 | + |
|
| 13 | +import GHC.Exts
|
|
| 14 | + |
|
| 15 | +loop :: Int# -> DoubleX2# -> Double# -> Double#
|
|
| 16 | +loop i v acc =
|
|
| 17 | + case unpackDoubleX2# v of
|
|
| 18 | + (# a1, _ #) ->
|
|
| 19 | + if isTrue# (a1 <## int2Double# i)
|
|
| 20 | + then acc
|
|
| 21 | + else case unpackDoubleX2# v of
|
|
| 22 | + (# a2, _ #) ->
|
|
| 23 | + if isTrue# (a2 *## 2.0## <## int2Double# i)
|
|
| 24 | + then acc *## 2.0##
|
|
| 25 | + else case unpackDoubleX2# v of
|
|
| 26 | + (# x, y #) -> loop (i -# 1#) v (acc +## (x *## y))
|
|
| 27 | +{-# NOINLINE loop #-} |
| ... | ... | @@ -6,3 +6,14 @@ test('regalloc_unit_tests', |
| 6 | 6 | [ignore_stderr, only_ways(['normal'])], extra_run_opts('"' + config.libdir + '"') ],
|
| 7 | 7 | compile_and_run,
|
| 8 | 8 | ['-package ghc'])
|
| 9 | + |
|
| 10 | +# The liveness fixpoint must iterate until the register formats converge, not
|
|
| 11 | +# just the sets of live registers (#27619). A vector register live at
|
|
| 12 | +# VecFormat 2 FmtDouble around the loop must not be recorded at FF64.
|
|
| 13 | +test('T27619',
|
|
| 14 | + [ unless(arch('x86_64'), skip),
|
|
| 15 | + when(not have_ncg(), skip),
|
|
| 16 | + only_ways(['normal']),
|
|
| 17 | + grep_errmsg(r'%vV128_\S+ :: FF64') ],
|
|
| 18 | + compile,
|
|
| 19 | + ['-O -fno-cse -ddump-asm-liveness']) |