Simon Jakobi pushed to branch wip/sjakobi/T25233 at Glasgow Haskell Compiler / GHC
Commits:
-
05c9684d
by Simon Jakobi at 2026-09-02T14:09:29+02:00
9 changed files:
- + changelog.d/ncg-x86-bit-test-instructions
- compiler/GHC/CmmToAsm/X86/CodeGen.hs
- compiler/GHC/CmmToAsm/X86/Instr.hs
- compiler/GHC/CmmToAsm/X86/Ppr.hs
- + testsuite/tests/codeGen/should_gen_asm/T25233.asm
- + testsuite/tests/codeGen/should_gen_asm/T25233.hs
- + testsuite/tests/codeGen/should_gen_asm/T25233b.asm
- + testsuite/tests/codeGen/should_gen_asm/T25233b.cmm
- testsuite/tests/codeGen/should_gen_asm/all.T
Changes:
| 1 | +section: compiler
|
|
| 2 | +synopsis: The x86 native code generator now uses the bit-test instructions
|
|
| 3 | + ``btr``/``bts``/``btc`` to clear, set or complement a single bit
|
|
| 4 | +description:
|
|
| 5 | + Cmm patterns such as ``x & ~(1 << i)``, ``x | (1 << i)`` and
|
|
| 6 | + ``x ^ (1 << i)`` now compile to a single ``btr``/``bts``/``btc``
|
|
| 7 | + instruction instead of a mov/shl/not/and-style sequence, matching what C
|
|
| 8 | + compilers produce. The same applies to the literal masks that constant
|
|
| 9 | + folding produces from these patterns when ``i`` is constant, in the cases
|
|
| 10 | + where the mask doesn't fit in an imm32 operand.
|
|
| 11 | + |
|
| 12 | + For a variable bit index this applies only when the shift is unchecked,
|
|
| 13 | + as with ``uncheckedShiftL#`` or ``Data.Bits.unsafeShiftL``. The
|
|
| 14 | + bounds-checked ``shiftL`` โ used, for example, by the default
|
|
| 15 | + implementations of ``clearBit``, ``setBit`` and ``complementBit`` โ
|
|
| 16 | + wraps the shift in a bounds mask that this optimisation does not see
|
|
| 17 | + through. With a literal index, the bounds mask is constant-folded away,
|
|
| 18 | + so the checked operations benefit too.
|
|
| 19 | +mrs: !16311
|
|
| 20 | +issues: #25233 |
| ... | ... | @@ -1442,6 +1442,22 @@ getRegister' platform is32Bit (CmmMachOp mop [x]) = do -- unary MachOps |
| 1442 | 1442 | (PUNPCKLQDQ fmt (OpReg dst) dst)
|
| 1443 | 1443 | )
|
| 1444 | 1444 | |
| 1445 | +-- Use the bit-test instructions btr/bts/btc for clearing, setting and
|
|
| 1446 | +-- complementing a single bit: e.g. x .&. complement (1 `shiftL` i) is btr.
|
|
| 1447 | +-- See Note [Bit-test instructions].
|
|
| 1448 | +getRegister' platform is32Bit (CmmMachOp (MO_And w) [x, y])
|
|
| 1449 | + | bitTestOpWidthOK is32Bit w
|
|
| 1450 | + , Just (opnd, ix) <- clearBitArgs_maybe platform w x y
|
|
| 1451 | + = genBitTestCode (intFormat w) BTR opnd ix
|
|
| 1452 | +getRegister' platform is32Bit (CmmMachOp (MO_Or w) [x, y])
|
|
| 1453 | + | bitTestOpWidthOK is32Bit w
|
|
| 1454 | + , Just (opnd, ix) <- setBitArgs_maybe platform w x y
|
|
| 1455 | + = genBitTestCode (intFormat w) BTS opnd ix
|
|
| 1456 | +getRegister' platform is32Bit (CmmMachOp (MO_Xor w) [x, y])
|
|
| 1457 | + | bitTestOpWidthOK is32Bit w
|
|
| 1458 | + , Just (opnd, ix) <- setBitArgs_maybe platform w x y
|
|
| 1459 | + = genBitTestCode (intFormat w) BTC opnd ix
|
|
| 1460 | + |
|
| 1445 | 1461 | getRegister' platform is32Bit (CmmMachOp mop [x, y]) = do -- dyadic MachOps
|
| 1446 | 1462 | sse4_1 <- sse4_1Enabled
|
| 1447 | 1463 | sse4_2 <- sse4_2Enabled
|
| ... | ... | @@ -5883,6 +5899,140 @@ genTrivialCode rep instr a b = do |
| 5883 | 5899 | instr b_op dst
|
| 5884 | 5900 | return (Any rep code)
|
| 5885 | 5901 | |
| 5902 | +{- Note [Bit-test instructions]
|
|
| 5903 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
| 5904 | +x86 has dedicated instructions for clearing (btr), setting (bts) and
|
|
| 5905 | +complementing (btc) a single bit whose index is given in a register. We use
|
|
| 5906 | +them for Cmm patterns such as
|
|
| 5907 | + |
|
| 5908 | + x & ~(1 << i) ==> btr i, x (#25233)
|
|
| 5909 | + |
|
| 5910 | +replacing a mov/shl/not/and sequence with a single instruction. The
|
|
| 5911 | +shift-count register operand of shl is masked modulo the operand width, and
|
|
| 5912 | +the bit-offset register operand of btr/bts/btc is masked the same way, so
|
|
| 5913 | +the replacement is faithful even for out-of-range i (where the Cmm shift is
|
|
| 5914 | +in any case undefined).
|
|
| 5915 | + |
|
| 5916 | +The bit-offset operand of these instructions must be an immediate or a
|
|
| 5917 | +register. When the bit index is a literal, no shift reaches the NCG:
|
|
| 5918 | +constant folding has already turned the whole mask into a literal. If that
|
|
| 5919 | +mask fits in an imm32, we keep the ordinary and/or/xor with an immediate:
|
|
| 5920 | +it has the same latency and better throughput (more execution ports) than
|
|
| 5921 | +the bit-test instructions, and at worst two bytes of extra code size for bit
|
|
| 5922 | +indices 7..30.
|
|
| 5923 | +But a W64 mask touching the upper bits, e.g. ~(1 << 40), would have to be moved
|
|
| 5924 | +into a register first. For such masks we recognise the folded literal itself
|
|
| 5925 | +(exactly one bit clear resp. set) and emit btr/bts/btc with an immediate
|
|
| 5926 | +bit offset.
|
|
| 5927 | + |
|
| 5928 | +We restrict the pattern to W32 and native-width W64: the instructions do not
|
|
| 5929 | +exist at width 8, and sub-word Cmm operations at W8/W16 are rare enough that
|
|
| 5930 | +they are not worth the extra care.
|
|
| 5931 | +-}
|
|
| 5932 | + |
|
| 5933 | +-- | Match @1 << i@, returning @i@.
|
|
| 5934 | +--
|
|
| 5935 | +-- The returned expression is always at word width ('machOpArgReps' fixes
|
|
| 5936 | +-- shift amounts at 'wordWidth'). See Note [Bit-test instructions].
|
|
| 5937 | +singleBit_maybe :: CmmExpr -> Maybe CmmExpr
|
|
| 5938 | +singleBit_maybe (CmmMachOp (MO_Shl _) [CmmLit (CmmInt 1 _), i]) = Just i
|
|
| 5939 | +singleBit_maybe _ = Nothing
|
|
| 5940 | + |
|
| 5941 | +-- | If exactly one bit of @m@, taken at width @w@, is set, return its index.
|
|
| 5942 | +--
|
|
| 5943 | +-- See Note [Bit-test instructions].
|
|
| 5944 | +setBitLit_maybe :: Width -> Integer -> Maybe Int
|
|
| 5945 | +setBitLit_maybe w m
|
|
| 5946 | + | popCount m' == 1 = Just (countTrailingZeros m')
|
|
| 5947 | + | otherwise = Nothing
|
|
| 5948 | + where
|
|
| 5949 | + -- w <= W64 in this X86-specific code, so a Word64 suffices.
|
|
| 5950 | + m' = fromInteger (narrowU w m) :: Word64
|
|
| 5951 | + |
|
| 5952 | +-- | If exactly one bit of @m@, taken at width @w@, is clear, return its
|
|
| 5953 | +-- index.
|
|
| 5954 | +--
|
|
| 5955 | +-- See Note [Bit-test instructions].
|
|
| 5956 | +clearBitLit_maybe :: Width -> Integer -> Maybe Int
|
|
| 5957 | +clearBitLit_maybe w m = setBitLit_maybe w (complement m)
|
|
| 5958 | + |
|
| 5959 | +bitTestOpWidthOK :: Bool -> Width -> Bool
|
|
| 5960 | +bitTestOpWidthOK is32Bit w = w == W32 || (w == W64 && not is32Bit)
|
|
| 5961 | + |
|
| 5962 | +-- | The bit-offset operand of a bit-test instruction (btr/bts/btc).
|
|
| 5963 | +data BitIndex
|
|
| 5964 | + = BitIndexReg CmmExpr -- ^ variable index, computed into a register
|
|
| 5965 | + | BitIndexImm Int -- ^ literal index, emitted as an immediate
|
|
| 5966 | + |
|
| 5967 | +-- | Match the operands of a single-bit set or complement operation: one
|
|
| 5968 | +-- operand is a mask @1 << i@, or a literal with exactly one bit set that
|
|
| 5969 | +-- does not fit in an imm32. Returns the other operand and the bit index.
|
|
| 5970 | +--
|
|
| 5971 | +-- Both operand orders are matched: constant folding canonicalizes literals
|
|
| 5972 | +-- to the right (see 'GHC.Cmm.Opt.cmmMachOpFoldM'), but e.g. hand-written
|
|
| 5973 | +-- .cmm code reaches the NCG unfolded.
|
|
| 5974 | +--
|
|
| 5975 | +-- See Note [Bit-test instructions].
|
|
| 5976 | +setBitArgs_maybe :: Platform -> Width -> CmmExpr -> CmmExpr
|
|
| 5977 | + -> Maybe (CmmExpr, BitIndex)
|
|
| 5978 | +setBitArgs_maybe platform w x y = go x y `mplus` go y x
|
|
| 5979 | + where
|
|
| 5980 | + go opnd mask
|
|
| 5981 | + | Just i <- singleBit_maybe mask
|
|
| 5982 | + = Just (opnd, BitIndexReg i)
|
|
| 5983 | + | CmmLit lit@(CmmInt m _) <- mask
|
|
| 5984 | + , Just i <- setBitLit_maybe w m
|
|
| 5985 | + , not (is32BitLit platform lit)
|
|
| 5986 | + = Just (opnd, BitIndexImm i)
|
|
| 5987 | + | otherwise
|
|
| 5988 | + = Nothing
|
|
| 5989 | + |
|
| 5990 | +-- | As 'setBitArgs_maybe', for a single-bit clear operation: the mask is
|
|
| 5991 | +-- @~(1 << i)@, or a literal with exactly one bit clear.
|
|
| 5992 | +clearBitArgs_maybe :: Platform -> Width -> CmmExpr -> CmmExpr
|
|
| 5993 | + -> Maybe (CmmExpr, BitIndex)
|
|
| 5994 | +clearBitArgs_maybe platform w x y = go x y `mplus` go y x
|
|
| 5995 | + where
|
|
| 5996 | + go opnd mask
|
|
| 5997 | + | CmmMachOp (MO_Not _) [b] <- mask
|
|
| 5998 | + , Just i <- singleBit_maybe b
|
|
| 5999 | + = Just (opnd, BitIndexReg i)
|
|
| 6000 | + | CmmLit lit@(CmmInt m _) <- mask
|
|
| 6001 | + , Just i <- clearBitLit_maybe w m
|
|
| 6002 | + , not (is32BitLit platform lit)
|
|
| 6003 | + = Just (opnd, BitIndexImm i)
|
|
| 6004 | + | otherwise
|
|
| 6005 | + = Nothing
|
|
| 6006 | + |
|
| 6007 | +-- | Generate code for @dst := x@ followed by a bit-test instruction
|
|
| 6008 | +-- (btr/bts/btc).
|
|
| 6009 | +--
|
|
| 6010 | +-- See Note [Bit-test instructions].
|
|
| 6011 | +genBitTestCode :: Format -> (Format -> Operand -> Operand -> Instr)
|
|
| 6012 | + -> CmmExpr -> BitIndex -> NatM Register
|
|
| 6013 | +genBitTestCode rep instr x (BitIndexImm i) = do
|
|
| 6014 | + x_code <- getAnyReg x
|
|
| 6015 | + let code dst = x_code dst `snocOL` instr rep (OpImm (ImmInt i)) (OpReg dst)
|
|
| 6016 | + return (Any rep code)
|
|
| 6017 | +genBitTestCode rep instr x (BitIndexReg i) = do
|
|
| 6018 | + (i_reg, i_code) <- getNonClobberedReg i
|
|
| 6019 | + x_code <- getAnyReg x
|
|
| 6020 | + tmp <- getNewRegNat rep
|
|
| 6021 | + let
|
|
| 6022 | + -- As in genTrivialCode, 'i' must stay alive across the computation of
|
|
| 6023 | + -- 'x' into dst, so save it in a temporary if dst holds 'i'.
|
|
| 6024 | + code dst
|
|
| 6025 | + | dst == i_reg =
|
|
| 6026 | + i_code `appOL`
|
|
| 6027 | + unitOL (MOV rep (OpReg i_reg) (OpReg tmp)) `appOL`
|
|
| 6028 | + x_code dst `snocOL`
|
|
| 6029 | + instr rep (OpReg tmp) (OpReg dst)
|
|
| 6030 | + | otherwise =
|
|
| 6031 | + i_code `appOL`
|
|
| 6032 | + x_code dst `snocOL`
|
|
| 6033 | + instr rep (OpReg i_reg) (OpReg dst)
|
|
| 6034 | + return (Any rep code)
|
|
| 6035 | + |
|
| 5886 | 6036 | regClashesWithOp :: Reg -> Operand -> Bool
|
| 5887 | 6037 | reg `regClashesWithOp` OpReg reg2 = reg == reg2
|
| 5888 | 6038 | reg `regClashesWithOp` OpAddr amode = any (==reg) (addrModeRegs amode)
|
| ... | ... | @@ -193,6 +193,12 @@ data Instr |
| 193 | 193 | | SHLD Format Operand{-amount-} Operand Operand
|
| 194 | 194 | |
| 195 | 195 | | BT Format Imm Operand
|
| 196 | + -- | Bit test-and-reset
|
|
| 197 | + | BTR Format Operand{- ^ bit offset (imm/reg) -} Operand
|
|
| 198 | + -- | Bit set
|
|
| 199 | + | BTS Format Operand{- ^ bit offset (imm/reg) -} Operand
|
|
| 200 | + -- | Bit complement
|
|
| 201 | + | BTC Format Operand{- ^ bit offset (imm/reg) -} Operand
|
|
| 196 | 202 | | NOP
|
| 197 | 203 | |
| 198 | 204 | |
| ... | ... | @@ -496,6 +502,9 @@ regUsageOfInstr platform instr |
| 496 | 502 | SHLD fmt imm dst1 dst2 -> usageRMM fmt imm dst1 dst2
|
| 497 | 503 | SHRD fmt imm dst1 dst2 -> usageRMM fmt imm dst1 dst2
|
| 498 | 504 | BT fmt _ src -> mkRUR (use_R fmt src [])
|
| 505 | + BTR fmt off dst -> usageRM fmt off dst
|
|
| 506 | + BTS fmt off dst -> usageRM fmt off dst
|
|
| 507 | + BTC fmt off dst -> usageRM fmt off dst
|
|
| 499 | 508 | |
| 500 | 509 | PUSH fmt op -> mkRUR (use_R fmt op [])
|
| 501 | 510 | POP fmt op -> mkRU [] (def_W fmt op)
|
| ... | ... | @@ -830,6 +839,9 @@ patchRegsOfInstr platform instr env |
| 830 | 839 | SHLD fmt imm dst1 dst2 -> patch2 (SHLD fmt imm) dst1 dst2
|
| 831 | 840 | SHRD fmt imm dst1 dst2 -> patch2 (SHRD fmt imm) dst1 dst2
|
| 832 | 841 | BT fmt imm src -> patch1 (BT fmt imm) src
|
| 842 | + BTR fmt off dst -> patch2 (BTR fmt) off dst
|
|
| 843 | + BTS fmt off dst -> patch2 (BTS fmt) off dst
|
|
| 844 | + BTC fmt off dst -> patch2 (BTC fmt) off dst
|
|
| 833 | 845 | TEST fmt src dst -> patch2 (TEST fmt) src dst
|
| 834 | 846 | CMP fmt src dst -> patch2 (CMP fmt) src dst
|
| 835 | 847 | PUSH fmt op -> patch1 (PUSH fmt) op
|
| ... | ... | @@ -862,6 +862,15 @@ pprInstr platform i = case i of |
| 862 | 862 | BT format imm src
|
| 863 | 863 | -> pprFormatImmOp (text "bt") format imm src
|
| 864 | 864 | |
| 865 | + BTR format off dst
|
|
| 866 | + -> pprFormatOpOp (text "btr") format off dst
|
|
| 867 | + |
|
| 868 | + BTS format off dst
|
|
| 869 | + -> pprFormatOpOp (text "bts") format off dst
|
|
| 870 | + |
|
| 871 | + BTC format off dst
|
|
| 872 | + -> pprFormatOpOp (text "btc") format off dst
|
|
| 873 | + |
|
| 865 | 874 | CMP format src dst
|
| 866 | 875 | | isFloatFormat format -> pprFormatOpOp (text "ucomi") format src dst -- SSE2
|
| 867 | 876 | | otherwise -> pprFormatOpOp (text "cmp") format src dst
|
| 1 | +btrq
|
|
| 2 | +btsq
|
|
| 3 | +btcq
|
|
| 4 | +btrl
|
|
| 5 | +btsl
|
|
| 6 | +btcl
|
|
| 7 | +btrq $40,
|
|
| 8 | +btsq $40,
|
|
| 9 | +btcq $40, |
| 1 | +{-# LANGUAGE MagicHash #-}
|
|
| 2 | + |
|
| 3 | +-- Check that clearing/setting/complementing a single, variable bit
|
|
| 4 | +-- uses the btr/bts/btc instructions (#25233).
|
|
| 5 | +module T25233 where
|
|
| 6 | + |
|
| 7 | +import GHC.Exts
|
|
| 8 | + |
|
| 9 | +myClearBit :: Word# -> Int# -> Word#
|
|
| 10 | +myClearBit x i = x `and#` not# (1## `uncheckedShiftL#` i)
|
|
| 11 | + |
|
| 12 | +mySetBit :: Word# -> Int# -> Word#
|
|
| 13 | +mySetBit x i = x `or#` (1## `uncheckedShiftL#` i)
|
|
| 14 | + |
|
| 15 | +myComplementBit :: Word# -> Int# -> Word#
|
|
| 16 | +myComplementBit x i = x `xor#` (1## `uncheckedShiftL#` i)
|
|
| 17 | + |
|
| 18 | +myClearBit32 :: Word32# -> Int# -> Word32#
|
|
| 19 | +myClearBit32 x i =
|
|
| 20 | + x `andWord32#` notWord32# (wordToWord32# 1## `uncheckedShiftLWord32#` i)
|
|
| 21 | + |
|
| 22 | +mySetBit32 :: Word32# -> Int# -> Word32#
|
|
| 23 | +mySetBit32 x i = x `orWord32#` (wordToWord32# 1## `uncheckedShiftLWord32#` i)
|
|
| 24 | + |
|
| 25 | +myComplementBit32 :: Word32# -> Int# -> Word32#
|
|
| 26 | +myComplementBit32 x i =
|
|
| 27 | + x `xorWord32#` (wordToWord32# 1## `uncheckedShiftLWord32#` i)
|
|
| 28 | + |
|
| 29 | +-- With a constant bit index >= 32, the mask constant-folds to a literal
|
|
| 30 | +-- that does not fit in an imm32, so a bit-test instruction with an
|
|
| 31 | +-- immediate offset is used.
|
|
| 32 | +myClearBit40 :: Word# -> Word#
|
|
| 33 | +myClearBit40 x = x `and#` not# (1## `uncheckedShiftL#` 40#)
|
|
| 34 | + |
|
| 35 | +mySetBit40 :: Word# -> Word#
|
|
| 36 | +mySetBit40 x = x `or#` (1## `uncheckedShiftL#` 40#)
|
|
| 37 | + |
|
| 38 | +myComplementBit40 :: Word# -> Word#
|
|
| 39 | +myComplementBit40 x = x `xor#` (1## `uncheckedShiftL#` 40#) |
| 1 | +btrq $40,
|
|
| 2 | +btsq $40,
|
|
| 3 | +btcq $40, |
| 1 | +#include "Cmm.h"
|
|
| 2 | + |
|
| 3 | +// Single-bit literal masks written on the left of the operator. Constant
|
|
| 4 | +// folding canonicalizes literals to the right, so these shapes only reach
|
|
| 5 | +// the NCG from hand-written Cmm like this (#25233).
|
|
| 6 | + |
|
| 7 | +clearBit40 (W_ x) {
|
|
| 8 | + return ((0xFFFFFEFFFFFFFFFF :: bits64) & x);
|
|
| 9 | +}
|
|
| 10 | + |
|
| 11 | +setBit40 (W_ x) {
|
|
| 12 | + return ((0x10000000000 :: bits64) | x);
|
|
| 13 | +}
|
|
| 14 | + |
|
| 15 | +complementBit40 (W_ x) {
|
|
| 16 | + return ((0x10000000000 :: bits64) ^ x);
|
|
| 17 | +} |
| ... | ... | @@ -23,6 +23,10 @@ test('avx512-int64-minmax', [unless(arch('x86_64'), skip), |
| 23 | 23 | when(unregisterised(), skip)], compile_grep_asm, ['hs', True, '-mavx512vl'])
|
| 24 | 24 | test('avx512-word64-minmax', [unless(arch('x86_64'), skip),
|
| 25 | 25 | when(unregisterised(), skip)], compile_grep_asm, ['hs', True, '-mavx512vl'])
|
| 26 | +test('T25233', [unless(arch('x86_64'), skip),
|
|
| 27 | + when(unregisterised(), skip)], compile_grep_asm, ['hs', True, '-O'])
|
|
| 28 | +test('T25233b', [unless(arch('x86_64'), skip),
|
|
| 29 | + when(unregisterised(), skip)], compile_grep_asm, ['cmm', True, ''])
|
|
| 26 | 30 | is_aarch64_codegen = [
|
| 27 | 31 | unless(arch('aarch64'), skip),
|
| 28 | 32 | when(unregisterised(), skip),
|