Marge Bot pushed to branch master at Glasgow Haskell Compiler / GHC
Commits:
5ebcfb57 by Benjamin Maurer at 2026-03-26T03:54:02-04:00
Generate assembly on x86 for word2float (#22252)
We used to emit C function call for MO_UF_Conv primitive.
Now emits direct assembly instead.
Co-Authored-By: Sylvain Henry
Co-Authored-By: Claude Sonnet 4.6
- - - - -
6 changed files:
- compiler/GHC/Cmm/Node.hs
- compiler/GHC/CmmToAsm/X86/CodeGen.hs
- testsuite/tests/codeGen/should_run/Word2Float32.hs
- testsuite/tests/codeGen/should_run/Word2Float32.stdout
- testsuite/tests/codeGen/should_run/Word2Float64.hs
- testsuite/tests/codeGen/should_run/Word2Float64.stdout
Changes:
=====================================
compiler/GHC/Cmm/Node.hs
=====================================
@@ -417,8 +417,8 @@ instance NonLocal CmmNode where
--------------------------------------------------
-- Various helper types
-type CmmActual = CmmExpr
-type CmmFormal = LocalReg
+type CmmActual = CmmExpr -- ^ Usually used to refer to arguments
+type CmmFormal = LocalReg -- ^ Usually used to refer to result registers.
type UpdFrameOffset = ByteOff
=====================================
compiler/GHC/CmmToAsm/X86/CodeGen.hs
=====================================
@@ -6714,10 +6714,172 @@ genClz bid width dst src = do
-- W8/W16 cases because the 'MOV' insn already
-- took care of implicitly clearing the upper bits
+{-
+Note [Word-to-float conversion on x86-64]
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+CVTSI2SD/CVTSI2SS treat their source as a *signed* integer, so an
+unsigned Word with the MSB set would yield a negative float.
+
+We use a halve-and-double trick:
+ 1. If src < 2^63 (MSB clear): convert directly; the signed and
+ unsigned interpretations agree.
+ 2. If src >= 2^63 (MSB set):
+ (a) Compute tmp = (src `shiftR` 1) .|. (src .&. 1)
+ which halves src while preserving the LSB as a "round bit".
+ (b) Convert tmp as a signed integer (its MSB is now clear).
+ (c) Double the float result.
+
+The round bit in step (a) is crucial for correct rounding. Without
+it, adjacent even and odd large values would produce the same float.
+With it, the conversion in step (b) sees the correct rounding
+information, and doubling in step (c) scales back to the right range.
+
+Example (Float64, src = 2^64 - 1 = 0xFFFF_FFFF_FFFF_FFFF):
+ tmp = 0x7FFF_FFFF_FFFF_FFFF | 1 = 0x7FFF_FFFF_FFFF_FFFF
+ float64(tmp) rounds to 2^63 ≈ 9.2234e18
+ 2 × 9.2234e18 = 1.8447e19 (= float64(2^64 - 1)) ✓
+
+Note [Word-to-float64 conversion on i386]
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+On 32-bit x86, StgWord is 32 bits. CVTSI2SD converts a *signed*
+32-bit integer, so inputs with the MSB set look negative.
+
+Trick: add 2^31 to flip the MSB, convert as signed, then add back 2^31 as a
+Float64 constant.
+
+Let src' = src + 2^31 (mod 2^32):
+ src in [0, 2^31): src' in [2^31, 2^32), signed value = src - 2^31.
+ CVTSI2SD gives src - 2^31. + 2^31.0 → src ✓
+ src = 2^31: src' = 0 (wraps). CVTSI2SD gives 0.0. + 2^31.0 → 2^31 ✓
+ src in (2^31, 2^32): src' = src - 2^31 ∈ (0, 2^31), positive.
+ CVTSI2SD gives src - 2^31. + 2^31.0 → src ✓
+
+The constant 2^31 is materialised without a memory load: 0x4F000000
+is the IEEE 754 float32 bit-pattern for 2^31; a MOVD + CVTSS2SD
+gives the exact float64 value.
+
+Note [Word-to-float32 conversion on i386]
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+On 32-bit x86, StgWord is 32 bits. CVTSI2SS converts a *signed*
+32-bit integer, so values >= 2^31 would convert incorrectly.
+
+We split the 32-bit unsigned value into its high and low 16-bit halves
+and convert each separately:
+ result = float32(high16) × 65536.0 + float32(low16)
+
+Both halves are in [0, 65535], within float32's exact integer range
+(24-bit mantissa covers integers up to 2^24 = 16777216 > 65535).
+Multiplying by 65536.0 = 2^16 is exact (no mantissa bits consumed).
+The final addition rounds to the nearest float32, matching a direct
+uint32→float32 conversion.
+
+The constant 65536.0 (= 0x47800000 in float32 bit-pattern) is loaded
+via a MOV + MOVD, avoiding a memory load.
+-}
+
genWordToFloat :: BlockId -> Width -> CmmFormal -> CmmActual -> NatM InstrBlock
-genWordToFloat bid width dst src =
- -- TODO: generate assembly instead
- genPrimCCall bid (word2FloatLabel width) [dst] [src]
+genWordToFloat bid width dst src = do
+ is32Bit <- is32BitPlatform
+ platform <- getPlatform
+
+ let srcFormat = intFormat $ cmmExprWidth platform src
+ let dst_r = getLocalRegReg dst
+ let conv = case width of
+ W64 -> CVTSI2SD
+ W32 -> CVTSI2SS
+ _ -> pprPanic "genWordToFloat: unsupported width" (ppr width)
+ let dstFormat = floatFormat width
+
+ (src_r, code_src) <- getSomeReg src
+
+ if is32Bit
+ then case (srcFormat, width) of
+ (II32, W64) -> do
+ -- See Note [Word-to-float64 conversion on i386]
+ cst_r <- getNewRegNat srcFormat
+ cst_v <- getNewRegNat dstFormat
+ flip_r <- getNewRegNat srcFormat
+ return $ code_src `appOL` toOL
+ [ MOV srcFormat (OpImm (ImmInt 0x4F000000)) (OpReg cst_r) -- load the constant
+ , MOVD srcFormat (floatFormat W32) (OpReg cst_r) (OpReg cst_v)
+ , CVTSS2SD cst_v cst_v
+ , MOV srcFormat (OpReg src_r) (OpReg flip_r) -- copy src (modified below)
+ , ADD srcFormat (OpImm $ ImmInteger 0x80000000) (OpReg flip_r) -- flip_r = flip MSB(src)
+ -- XOR dst_r with itself to avoid a false dependency: CVTSI2SD
+ -- (SSE2) only writes the lower 64 bits of the destination XMM
+ -- register, leaving the upper bits unchanged. That creates a
+ -- dependency on the old value of dst_r. Zeroing it first breaks
+ -- the dependency chain.
+ , XOR dstFormat (OpReg dst_r) (OpReg dst_r)
+ , conv srcFormat (OpReg flip_r) dst_r
+ , ADD dstFormat (OpReg cst_v) (OpReg dst_r) -- +2147483648.0
+ ]
+ (II32, W32) -> do
+ -- See Note [Word-to-float32 conversion on i386]
+ tmp_v <- getNewRegNat dstFormat
+ cst_v <- getNewRegNat dstFormat
+ cst_r <- getNewRegNat srcFormat
+ high_r <- getNewRegNat srcFormat
+ low_r <- getNewRegNat srcFormat
+ return $ code_src `appOL` toOL
+ [ MOV srcFormat (OpImm (ImmInt 0x47800000)) (OpReg cst_r) -- load the constant
+ , MOVD srcFormat dstFormat (OpReg cst_r) (OpReg cst_v)
+ , MOVZxL II16 (OpReg src_r) (OpReg low_r) -- low_r = low 16 bits
+ , MOV srcFormat (OpReg src_r) (OpReg high_r) -- copy src (modified below)
+ , SHR srcFormat (OpImm $ ImmInt 16) (OpReg high_r) -- high_r = high 16 bits
+ , conv srcFormat (OpReg high_r) dst_r -- dst_r = float(high)
+ , MUL dstFormat (OpReg cst_v) (OpReg dst_r) -- dst_r = float(high) * 65536.0
+ -- XOR tmp_v to avoid a false dependency on its previous value
+ -- before the CVTSI2SS below (same reasoning as in the W64 case).
+ , XOR dstFormat (OpReg tmp_v) (OpReg tmp_v)
+ , conv srcFormat (OpReg low_r) tmp_v -- tmp_v = float(low)
+ , ADD dstFormat (OpReg tmp_v) (OpReg dst_r) -- dst_r = float(high)*65536.0 + float(low)
+ ]
+ _ -> panic ("genWordToFloat: unsupported source operand format: " ++ show srcFormat)
+ else do
+ -- See Note [Word-to-float conversion on x86-64]
+ half_r <- getNewRegNat srcFormat
+ round_r <- getNewRegNat srcFormat
+
+ lblLarge <- getBlockIdNat
+ lblSmall <- getBlockIdNat
+ lblAfter <- getBlockIdNat
+
+ -- We're building a diamond CFG:
+ -- bid -> lblSmall -> lblAfter -> origSucc
+ -- \-> lblLarge ->/
+ -- addImmediateSuccessorNat moves bid's original successor to lblAfter,
+ -- then we fix up the other edges.
+ addImmediateSuccessorNat bid lblAfter
+ -- Small values (MSB clear, i.e. < 2^63) are assumed more common in
+ -- practice, hence the higher weight on the lblSmall edge.
+ updateCfgNat ( addWeightEdge bid lblSmall 100
+ . addWeightEdge bid lblLarge 50
+ . addWeightEdge lblSmall lblAfter 1
+ . addWeightEdge lblLarge lblAfter 1
+ . delEdge bid lblAfter )
+
+ return $ appOL (code_src)
+ $ toOL
+ [ TEST srcFormat (OpReg src_r) (OpReg src_r)
+ , JXX NEG lblLarge
+ -- Adding this label to allow optimizations to either invert condition or just eliminate
+ , JXX ALWAYS lblSmall
+ , NEWBLOCK lblSmall
+ , conv srcFormat (OpReg src_r) dst_r -- direct conversion for src < 2^63
+ , JXX ALWAYS lblAfter
+ , NEWBLOCK lblLarge
+ -- Halve src, preserving the LSB as a round bit, then convert and double.
+ , MOV srcFormat (OpReg src_r) (OpReg half_r)
+ , SHR srcFormat (OpImm $ ImmInt 1) (OpReg half_r) -- half_r = src >> 1
+ , MOV srcFormat (OpReg src_r) (OpReg round_r) -- copy src (modified below)
+ , AND srcFormat (OpImm $ ImmInt 1) (OpReg round_r) -- round_r = src & 1 (round bit)
+ , OR srcFormat (OpReg round_r) (OpReg half_r) -- half_r = (src >> 1) | (src & 1)
+ , conv srcFormat (OpReg half_r) dst_r
+ , ADD dstFormat (OpReg dst_r) (OpReg dst_r) -- double the result
+ , JXX ALWAYS lblAfter
+ , NEWBLOCK lblAfter
+ ]
genAtomicRead :: Width -> MemoryOrdering -> LocalReg -> CmmExpr -> NatM InstrBlock
genAtomicRead width _mord dst addr = do
=====================================
testsuite/tests/codeGen/should_run/Word2Float32.hs
=====================================
@@ -1,7 +1,7 @@
{-# LANGUAGE MagicHash #-}
module Main (main) where
-import GHC.Exts (Double(D#), Float(F#), word2Double#, word2Float#)
+import GHC.Exts (Double(D#), Float(F#), Word(W#), word2Double#, word2Float#)
main :: IO ()
main = do
@@ -15,3 +15,13 @@ main = do
-- stored in a 32-bit IEEE floating-point value without loss of
-- precision
print (F# (word2Float# 16777216##))
+
+ -- We also want to check for sane behaviour for cases that lose precision
+ let W# max_word = (maxBound :: Word)
+ print (F# (word2Float# max_word))
+
+ -- 2^31 sits exactly at the MSB boundary exercised by the i386 algorithms
+ -- (see Notes [Word-to-float64 conversion on i386] and
+ -- [Word-to-float32 conversion on i386] in GHC.CmmToAsm.X86.CodeGen)
+ print (D# (word2Double# 2147483648##))
+ print (F# (word2Float# 2147483648##))
=====================================
testsuite/tests/codeGen/should_run/Word2Float32.stdout
=====================================
@@ -2,3 +2,6 @@
4.294967295e9
0.0
1.6777216e7
+4.2949673e9
+2.147483648e9
+2.1474836e9
=====================================
testsuite/tests/codeGen/should_run/Word2Float64.hs
=====================================
@@ -1,7 +1,7 @@
{-# LANGUAGE MagicHash #-}
module Main (main) where
-import GHC.Exts (Double(D#), Float(F#), word2Double#, word2Float#)
+import GHC.Exts (Double(D#), Float(F#), Word(W#), word2Double#, word2Float#)
main :: IO ()
main = do
@@ -15,3 +15,14 @@ main = do
-- stored in a 32-bit IEEE floating-point value without loss of
-- precision
print (F# (word2Float# 16777216##))
+
+ -- We also want to check for sane behaviour for cases that lose precision
+ let W# max_word = (maxBound :: Word)
+ print (F# (word2Float# max_word))
+ print (D# (word2Double# max_word))
+
+ -- 2^63 is the first value that requires the halve-and-double path
+ -- (see Note [Word-to-float conversion on x86-64] in GHC.CmmToAsm.X86.CodeGen)
+ let W# two63 = 0x8000000000000000
+ print (F# (word2Float# two63))
+ print (D# (word2Double# two63))
=====================================
testsuite/tests/codeGen/should_run/Word2Float64.stdout
=====================================
@@ -2,3 +2,7 @@
9.007199254740992e15
0.0
1.6777216e7
+1.8446744e19
+1.8446744073709552e19
+9.223372e18
+9.223372036854776e18
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/5ebcfb5767804895488da29f0852fa0e...
--
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/5ebcfb5767804895488da29f0852fa0e...
You're receiving this email because of your account on gitlab.haskell.org.