splitmix-0.1.1: src/System/Random/SplitMix.hs
-- |
-- /SplitMix/ is a splittable pseudorandom number generator (PRNG) that is quite fast.
--
-- Guy L. Steele, Jr., Doug Lea, and Christine H. Flood. 2014.
-- Fast splittable pseudorandom number generators. In Proceedings
-- of the 2014 ACM International Conference on Object Oriented
-- Programming Systems Languages & Applications (OOPSLA '14). ACM,
-- New York, NY, USA, 453-472. DOI:
-- <https://doi.org/10.1145/2660193.2660195>
--
-- The paper describes a new algorithm /SplitMix/ for /splittable/
-- pseudorandom number generator that is quite fast: 9 64 bit arithmetic/logical
-- operations per 64 bits generated.
--
-- /SplitMix/ is tested with two standard statistical test suites (DieHarder and
-- TestU01, this implementation only using the former) and it appears to be
-- adequate for "everyday" use, such as Monte Carlo algorithms and randomized
-- data structures where speed is important.
--
-- In particular, it __should not be used for cryptographic or security applications__,
-- because generated sequences of pseudorandom values are too predictable
-- (the mixing functions are easily inverted, and two successive outputs
-- suffice to reconstruct the internal state).
--
{-# LANGUAGE CPP #-}
{-# LANGUAGE Trustworthy #-}
module System.Random.SplitMix (
SMGen,
nextWord64,
nextWord32,
nextTwoWord32,
nextInt,
nextDouble,
nextFloat,
nextInteger,
splitSMGen,
-- * Generation
bitmaskWithRejection32,
bitmaskWithRejection32',
bitmaskWithRejection64,
bitmaskWithRejection64',
-- * Initialisation
mkSMGen,
initSMGen,
newSMGen,
seedSMGen,
seedSMGen',
unseedSMGen,
) where
import Data.Bits (complement, shiftL, shiftR, xor, (.&.), (.|.))
import Data.Bits.Compat (countLeadingZeros, popCount, zeroBits)
import Data.IORef (IORef, atomicModifyIORef, newIORef)
import Data.Word (Word32, Word64)
import System.IO.Unsafe (unsafePerformIO)
import System.Random.SplitMix.Init
#if defined(__HUGS__)
import Data.Word (Word)
#endif
#ifndef __HUGS__
import Control.DeepSeq (NFData (..))
#endif
-- $setup
-- >>> import Text.Read (readMaybe)
-- >>> import Data.List (unfoldr)
-- >>> import Text.Printf (printf)
-------------------------------------------------------------------------------
-- Generator
-------------------------------------------------------------------------------
-- | SplitMix generator state.
data SMGen = SMGen !Word64 !Word64 -- seed and gamma; gamma is odd
deriving Show
#ifndef __HUGS__
instance NFData SMGen where
rnf (SMGen _ _) = ()
#endif
-- |
--
-- >>> readMaybe "SMGen 1 1" :: Maybe SMGen
-- Just (SMGen 1 1)
--
-- >>> readMaybe "SMGen 1 2" :: Maybe SMGen
-- Nothing
--
-- >>> readMaybe (show (mkSMGen 42)) :: Maybe SMGen
-- Just (SMGen 9297814886316923340 13679457532755275413)
--
instance Read SMGen where
readsPrec d r = readParen (d > 10) (\r0 ->
[ (SMGen seed gamma, r3)
| ("SMGen", r1) <- lex r0
, (seed, r2) <- readsPrec 11 r1
, (gamma, r3) <- readsPrec 11 r2
, odd gamma
]) r
-------------------------------------------------------------------------------
-- Operations
-------------------------------------------------------------------------------
-- | Generate a 'Word64'.
--
-- >>> take 3 $ map (printf "%x") $ unfoldr (Just . nextWord64) (mkSMGen 1337) :: [String]
-- ["b5c19e300e8b07b3","d600e0e216c0ac76","c54efc3b3cc5af29"]
--
nextWord64 :: SMGen -> (Word64, SMGen)
nextWord64 (SMGen seed gamma) = (mix64 seed', SMGen seed' gamma)
where
seed' = seed `plus` gamma
-- | Generate 'Word32' by truncating 'nextWord64'.
--
-- @since 0.0.3
nextWord32 :: SMGen -> (Word32, SMGen)
nextWord32 g =
#ifdef __HUGS__
(fromIntegral $ w64 .&. 0xffffffff, g')
#else
(fromIntegral w64, g')
#endif
where
(w64, g') = nextWord64 g
-- | Generate two 'Word32'.
--
-- @since 0.0.3
nextTwoWord32 :: SMGen -> (Word32, Word32, SMGen)
nextTwoWord32 g =
#ifdef __HUGS__
(fromIntegral $ w64 `shiftR` 32, fromIntegral $ w64 .&. 0xffffffff, g')
#else
(fromIntegral $ w64 `shiftR` 32, fromIntegral w64, g')
#endif
where
(w64, g') = nextWord64 g
-- | Generate an 'Int'.
nextInt :: SMGen -> (Int, SMGen)
nextInt g = case nextWord64 g of
#ifdef __HUGS__
(w64, g') -> (fromIntegral $ w64 `shiftR` 32, g')
#else
(w64, g') -> (fromIntegral w64, g')
#endif
-- | Generate a 'Double' in @[0, 1)@ range.
--
-- >>> take 8 $ map (printf "%0.3f") $ unfoldr (Just . nextDouble) (mkSMGen 1337) :: [String]
-- ["0.710","0.836","0.771","0.409","0.297","0.527","0.589","0.067"]
--
nextDouble :: SMGen -> (Double, SMGen)
nextDouble g = case nextWord64 g of
(w64, g') -> (fromIntegral (w64 `shiftR` 11) * doubleUlp, g')
-- | Generate a 'Float' in @[0, 1)@ range.
--
-- >>> take 8 $ map (printf "%0.3f") $ unfoldr (Just . nextFloat) (mkSMGen 1337) :: [String]
-- ["0.057","0.089","0.237","0.383","0.680","0.320","0.826","0.007"]
--
-- @since 0.0.3
nextFloat :: SMGen -> (Float, SMGen)
nextFloat g = case nextWord32 g of
(w32, g') -> (fromIntegral (w32 `shiftR` 8) * floatUlp, g')
-- | Generate an 'Integer' in closed @[x, y]@ range.
nextInteger :: Integer -> Integer -> SMGen -> (Integer, SMGen)
nextInteger lo hi g = case compare lo hi of
LT -> let (i, g') = nextInteger' (hi - lo) g in (i + lo, g')
EQ -> (lo, g)
GT -> let (i, g') = nextInteger' (lo - hi) g in (i + hi, g')
-- invariant: first argument is positive
-- Essentially bitmaskWithRejection but for Integers.
--
nextInteger' :: Integer -> SMGen -> (Integer, SMGen)
nextInteger' range = loop
where
leadMask :: Word64
restDigits :: Word
(leadMask, restDigits) = go 0 range where
go :: Word -> Integer -> (Word64, Word)
go n x | x < two64 = (complement zeroBits `shiftR` countLeadingZeros (fromInteger x :: Word64), n)
| otherwise = go (n + 1) (x `shiftR` 64)
generate :: SMGen -> (Integer, SMGen)
generate g0 =
let (x, g') = nextWord64 g0
x' = x .&. leadMask
in go (fromIntegral x') restDigits g'
where
go :: Integer -> Word -> SMGen -> (Integer, SMGen)
go acc 0 g = acc `seq` (acc, g)
go acc n g =
let (x, g') = nextWord64 g
in go (acc * two64 + fromIntegral x) (n - 1) g'
loop g = let (x, g') = generate g
in if x > range
then loop g'
else (x, g')
two64 :: Integer
two64 = 2 ^ (64 :: Int)
-------------------------------------------------------------------------------
-- Splitting
-------------------------------------------------------------------------------
-- | Split a generator into a two uncorrelated generators.
splitSMGen :: SMGen -> (SMGen, SMGen)
splitSMGen (SMGen seed gamma) =
(SMGen seed'' gamma, SMGen (mix64 seed') (mixGamma seed''))
where
seed' = seed `plus` gamma
seed'' = seed' `plus` gamma
-------------------------------------------------------------------------------
-- Algorithm
-------------------------------------------------------------------------------
goldenGamma :: Word64
goldenGamma = 0x9e3779b97f4a7c15
floatUlp :: Float
floatUlp = 1.0 / fromIntegral (1 `shiftL` 24 :: Word32)
doubleUlp :: Double
doubleUlp = 1.0 / fromIntegral (1 `shiftL` 53 :: Word64)
-- Note: in JDK implementations the mix64 and mix64variant13
-- (which is inlined into mixGamma) are swapped.
mix64 :: Word64 -> Word64
mix64 z0 =
-- MurmurHash3Mixer
let z1 = shiftXorMultiply 33 0xff51afd7ed558ccd z0
z2 = shiftXorMultiply 33 0xc4ceb9fe1a85ec53 z1
z3 = shiftXor 33 z2
in z3
-- used only in mixGamma
mix64variant13 :: Word64 -> Word64
mix64variant13 z0 =
-- Better Bit Mixing - Improving on MurmurHash3's 64-bit Finalizer
-- http://zimbry.blogspot.fi/2011/09/better-bit-mixing-improving-on.html
--
-- Stafford's Mix13
let z1 = shiftXorMultiply 30 0xbf58476d1ce4e5b9 z0 -- MurmurHash3 mix constants
z2 = shiftXorMultiply 27 0x94d049bb133111eb z1
z3 = shiftXor 31 z2
in z3
mixGamma :: Word64 -> Word64
mixGamma z0 =
let z1 = mix64variant13 z0 .|. 1 -- force to be odd
n = popCount (z1 `xor` (z1 `shiftR` 1))
-- see: http://www.pcg-random.org/posts/bugs-in-splitmix.html
-- let's trust the text of the paper, not the code.
in if n >= 24
then z1
else z1 `xor` 0xaaaaaaaaaaaaaaaa
shiftXor :: Int -> Word64 -> Word64
shiftXor n w = w `xor` (w `shiftR` n)
shiftXorMultiply :: Int -> Word64 -> Word64 -> Word64
shiftXorMultiply n k w = shiftXor n w `mult` k
-------------------------------------------------------------------------------
-- Generation
-------------------------------------------------------------------------------
-- | /Bitmask with rejection/ method of generating subrange of 'Word32'.
--
-- @bitmaskWithRejection32 w32@ generates random numbers in closed-open
-- range of @[0, w32)@.
--
-- @since 0.0.3
bitmaskWithRejection32 :: Word32 -> SMGen -> (Word32, SMGen)
bitmaskWithRejection32 0 = error "bitmaskWithRejection32 0"
bitmaskWithRejection32 n = bitmaskWithRejection32' (n - 1)
{-# INLINEABLE bitmaskWithRejection32 #-}
-- | /Bitmask with rejection/ method of generating subrange of 'Word64'.
--
-- @bitmaskWithRejection64 w64@ generates random numbers in closed-open
-- range of @[0, w64)@.
--
-- >>> take 20 $ unfoldr (Just . bitmaskWithRejection64 5) (mkSMGen 1337)
-- [3,1,4,1,2,3,1,1,0,3,4,2,3,0,2,3,3,4,1,0]
--
-- @since 0.0.3
bitmaskWithRejection64 :: Word64 -> SMGen -> (Word64, SMGen)
bitmaskWithRejection64 0 = error "bitmaskWithRejection64 0"
bitmaskWithRejection64 n = bitmaskWithRejection64' (n - 1)
{-# INLINEABLE bitmaskWithRejection64 #-}
-- | /Bitmask with rejection/ method of generating subrange of 'Word32'.
--
-- @bitmaskWithRejection32' w32@ generates random numbers in closed-closed
-- range of @[0, w32]@.
--
-- @since 0.0.4
bitmaskWithRejection32' :: Word32 -> SMGen -> (Word32, SMGen)
bitmaskWithRejection32' range = go where
mask = complement zeroBits `shiftR` countLeadingZeros (range .|. 1)
go g = let (x, g') = nextWord32 g
x' = x .&. mask
in if x' > range
then go g'
else (x', g')
{-# INLINEABLE bitmaskWithRejection32' #-}
-- | /Bitmask with rejection/ method of generating subrange of 'Word64'.
--
-- @bitmaskWithRejection64' w64@ generates random numbers in closed-closed
-- range of @[0, w64]@.
--
-- >>> take 20 $ unfoldr (Just . bitmaskWithRejection64' 5) (mkSMGen 1337)
-- [3,1,4,1,2,3,1,1,0,3,4,5,2,3,0,2,3,5,3,4]
--
-- @since 0.0.4
bitmaskWithRejection64' :: Word64 -> SMGen -> (Word64, SMGen)
bitmaskWithRejection64' range = go where
mask = complement zeroBits `shiftR` countLeadingZeros range
go g = let (x, g') = nextWord64 g
x' = x .&. mask
in if x' > range
then go g'
else (x', g')
{-# INLINEABLE bitmaskWithRejection64' #-}
-------------------------------------------------------------------------------
-- Initialisation
-------------------------------------------------------------------------------
-- | Create 'SMGen' using seed and gamma.
--
-- >>> seedSMGen 2 2
-- SMGen 2 3
--
seedSMGen
:: Word64 -- ^ seed
-> Word64 -- ^ gamma
-> SMGen
seedSMGen seed gamma = SMGen seed (gamma .|. 1)
-- | Like 'seedSMGen' but takes a pair.
seedSMGen' :: (Word64, Word64) -> SMGen
seedSMGen' = uncurry seedSMGen
-- | Extract current state of 'SMGen'.
unseedSMGen :: SMGen -> (Word64, Word64)
unseedSMGen (SMGen seed gamma) = (seed, gamma)
-- | Preferred way to deterministically construct 'SMGen'.
--
-- >>> mkSMGen 42
-- SMGen 9297814886316923340 13679457532755275413
--
mkSMGen :: Word64 -> SMGen
mkSMGen s = SMGen (mix64 s) (mixGamma (s `plus` goldenGamma))
-- | Initialize 'SMGen' using entropy available on the system (time, ...)
initSMGen :: IO SMGen
initSMGen = fmap mkSMGen initialSeed
-- | Derive a new generator instance from the global 'SMGen' using 'splitSMGen'.
newSMGen :: IO SMGen
newSMGen = atomicModifyIORef theSMGen splitSMGen
theSMGen :: IORef SMGen
theSMGen = unsafePerformIO $ initSMGen >>= newIORef
{-# NOINLINE theSMGen #-}
-------------------------------------------------------------------------------
-- Hugs
-------------------------------------------------------------------------------
mult, plus :: Word64 -> Word64 -> Word64
#ifndef __HUGS__
mult = (*)
plus = (+)
#else
-- Hugs defines:
--
-- x * y = fromInteger (toInteger x * toInteger y)
-- x + y = fromInteger (toInteger x + toInteger y)
--
-- which obviously overflows in our use cases, as fromInteger doesn't truncate
--
mult x y = fromInteger ((toInteger x * toInteger y) `mod` 18446744073709551616)
plus x y = fromInteger ((toInteger x + toInteger y) `mod` 18446744073709551616)
#endif