1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269
|
{-# LANGUAGE BangPatterns, ScopedTypeVariables, ForeignFunctionInterface #-}
{-# OPTIONS_GHC -fwarn-unused-imports #-}
-- | A simple script to do some very basic timing of the RNGs.
module Main where
import System.Exit (exitSuccess, exitFailure)
import System.Environment
import System.Random
import System.CPUTime (getCPUTime)
import System.CPUTime.Rdtsc
import System.Console.GetOpt
import GHC.Conc
import Control.Concurrent
import Control.Monad
import Control.Exception
import Data.IORef
import Data.Word
import Data.List hiding (last,sum)
import Data.Int
import Data.List.Split hiding (split)
import Text.Printf
import Foreign.Ptr
import Foreign.C.Types
import Foreign.ForeignPtr
import Foreign.Storable (peek,poke)
import Prelude hiding (last,sum)
import BinSearch
----------------------------------------------------------------------------------------------------
-- Miscellaneous helpers:
-- Readable large integer printing:
commaint :: Show a => a -> String
commaint n = reverse $ concat $ intersperse "," $ chunk 3 $ reverse (show n)
padleft :: Int -> String -> String
padleft n str | length str >= n = str
padleft n str | otherwise = take (n - length str) (repeat ' ') ++ str
padright :: Int -> String -> String
padright n str | length str >= n = str
padright n str | otherwise = str ++ take (n - length str) (repeat ' ')
fmt_num :: (RealFrac a, PrintfArg a) => a -> String
fmt_num n =
if n < 100
then printf "%.2f" n
else commaint (round n :: Integer)
-- Measure clock frequency, spinning rather than sleeping to try to
-- stay on the same core.
measureFreq :: IO Int64
measureFreq = do
let second = 1000 * 1000 * 1000 * 1000 -- picoseconds are annoying
t1 <- rdtsc
start <- getCPUTime
let loop !n !last = do
t2 <- rdtsc
when (t2 < last) $ putStrLn $ "COUNTERS WRAPPED " ++ show (last, t2)
cput <- getCPUTime
if cput - start < second
then loop (n + 1) t2
else return (n, t2)
(n, t2) <- loop 0 t1
putStrLn $ " Approx getCPUTime calls per second: " ++ commaint (n :: Int64)
when (t2 < t1) $
putStrLn $
"WARNING: rdtsc not monotonically increasing, first " ++
show t1 ++ " then " ++ show t2 ++ " on the same OS thread"
return $ fromIntegral (t2 - t1)
----------------------------------------------------------------------------------------------------
-- Test overheads without actually generating any random numbers:
data NoopRNG = NoopRNG
instance RandomGen NoopRNG where
next g = (0, g)
genRange _ = (0, 0)
split g = (g, g)
-- An RNG generating only 0 or 1:
data BinRNG = BinRNG StdGen
instance RandomGen BinRNG where
next (BinRNG g) = (x `mod` 2, BinRNG g')
where
(x, g') = next g
genRange _ = (0, 1)
split (BinRNG g) = (BinRNG g1, BinRNG g2)
where
(g1, g2) = split g
----------------------------------------------------------------------------------------------------
-- Drivers to get random numbers repeatedly.
type Kern = Int -> Ptr Int -> IO ()
-- [2011.01.28] Changing this to take "count" and "accumulator ptr" as arguments:
-- foreign import ccall "cbits/c_test.c" blast_rands :: Kern
-- foreign import ccall "cbits/c_test.c" store_loop :: Kern
-- foreign import ccall unsafe "stdlib.hs" rand :: IO Int
{-# INLINE timeit #-}
timeit :: (Random a, RandomGen g) => Int -> Int64 -> String -> g -> (g -> (a,g)) -> IO ()
timeit numthreads freq msg gen nxt = do
counters <- forM [1 .. numthreads] (const $ newIORef (1 :: Int64))
tids <- forM counters $ \counter -> forkIO $ infloop counter (nxt gen)
threadDelay (1000 * 1000) -- One second
mapM_ killThread tids
finals <- mapM readIORef counters
let mean :: Double =
fromIntegral (foldl1 (+) finals) / fromIntegral numthreads
cycles_per :: Double = fromIntegral freq / mean
printResult (round mean :: Int64) msg cycles_per
where
infloop !counter (!_, !g) = do
incr counter
infloop counter (nxt g)
incr !counter
-- modifyIORef counter (+1) -- Not strict enough!
= do
c <- readIORef counter
let c' = c + 1
_ <- evaluate c'
writeIORef counter c'
-- This function times an IO function on one or more threads. Rather
-- than running a fixed number of iterations, it uses a binary search
-- to find out how many iterations can be completed in a second.
timeit_foreign :: Int -> Int64 -> String -> (Int -> Ptr Int -> IO ()) -> IO Int64
timeit_foreign numthreads freq msg ffn = do
ptr :: ForeignPtr Int <- mallocForeignPtr
let kern =
if numthreads == 1
then ffn
else replicate_kernel numthreads ffn
wrapped n = withForeignPtr ptr (kern $ fromIntegral n)
(n, t) <- binSearch False 1 (1.0, 1.05) wrapped
let total_per_second = round $ fromIntegral n * (1 / t)
cycles_per = fromIntegral freq * t / fromIntegral n
printResult total_per_second msg cycles_per
return total_per_second
-- This lifts a C kernel to operate simultaneously on N threads.
where
replicate_kernel :: Int -> Kern -> Kern
replicate_kernel nthreads kern n ptr = do
ptrs <- forM [1 .. nthreads] (const mallocForeignPtr)
tmpchan <- newChan
-- let childwork = ceiling$ fromIntegral n / fromIntegral nthreads
let childwork = n -- Keep it the same.. interested in per-thread throughput.
-- Fork/join pattern:
forM_ ptrs $ \pt ->
forkIO $
withForeignPtr pt $ \p -> do
kern (fromIntegral childwork) p
result <- peek p
writeChan tmpchan result
results <- forM [1 .. nthreads] $ \_ -> readChan tmpchan
-- Meaningless semantics here... sum the child ptrs and write to the input one:
poke ptr (foldl1 (+) results)
printResult :: Int64 -> String -> Double -> IO ()
printResult total msg cycles_per =
putStrLn $
" " ++
padleft 11 (commaint total) ++
" randoms generated " ++
padright 27 ("[" ++ msg ++ "]") ++
" ~ " ++ fmt_num cycles_per ++ " cycles/int"
----------------------------------------------------------------------------------------------------
-- Main Script
data Flag = NoC | Help
deriving (Show, Eq)
options :: [OptDescr Flag]
options =
[ Option ['h'] ["help"] (NoArg Help) "print program help"
, Option [] ["noC"] (NoArg NoC) "omit C benchmarks, haskell only"
]
main :: IO ()
main = do
argv <- getArgs
let (opts,_,other) = getOpt Permute options argv
unless (null other) $ do
putStrLn "ERROR: Unrecognized options: "
mapM_ putStr other
exitFailure
when (Help `elem` opts) $ do
putStr $ usageInfo "Benchmark random number generation" options
exitSuccess
putStrLn "\nHow many random numbers can we generate in a second on one thread?"
t1 <- rdtsc
t2 <- rdtsc
putStrLn (" Cost of rdtsc (ffi call): " ++ show (t2 - t1))
freq <- measureFreq
putStrLn $ " Approx clock frequency: " ++ commaint freq
let randInt = random :: RandomGen g => g -> (Int,g)
randWord16 = random :: RandomGen g => g -> (Word16,g)
randFloat = random :: RandomGen g => g -> (Float,g)
randCFloat = random :: RandomGen g => g -> (CFloat,g)
randDouble = random :: RandomGen g => g -> (Double,g)
randCDouble = random :: RandomGen g => g -> (CDouble,g)
randInteger = random :: RandomGen g => g -> (Integer,g)
randBool = random :: RandomGen g => g -> (Bool,g)
randChar = random :: RandomGen g => g -> (Char,g)
gen = mkStdGen 238523586
gamut th = do
putStrLn " First, timing System.Random.next:"
timeit th freq "constant zero gen" NoopRNG next
timeit th freq "System.Random stdGen/next" gen next
putStrLn "\n Second, timing System.Random.random at different types:"
timeit th freq "System.Random Ints" gen randInt
timeit th freq "System.Random Word16" gen randWord16
timeit th freq "System.Random Floats" gen randFloat
timeit th freq "System.Random CFloats" gen randCFloat
timeit th freq "System.Random Doubles" gen randDouble
timeit th freq "System.Random CDoubles" gen randCDouble
timeit th freq "System.Random Integers" gen randInteger
timeit th freq "System.Random Bools" gen randBool
timeit th freq "System.Random Chars" gen randChar
putStrLn "\n Next timing range-restricted System.Random.randomR:"
timeit th freq "System.Random Ints" gen (randomR (-100, 100::Int))
timeit th freq "System.Random Word16s" gen (randomR ( 100, 300::Word16))
timeit th freq "System.Random Floats" gen (randomR (-100, 100::Float))
timeit th freq "System.Random CFloats" gen (randomR (-100, 100::CFloat))
timeit th freq "System.Random Doubles" gen (randomR (-100, 100::Double))
timeit th freq "System.Random CDoubles" gen (randomR (-100, 100::CDouble))
timeit th freq "System.Random Integers" gen (randomR (-100, 100::Integer))
timeit th freq "System.Random Bools" gen (randomR (False, True::Bool))
timeit th freq "System.Random Chars" gen (randomR ('a', 'z'))
timeit th freq "System.Random BIG Integers" gen (randomR (0, (2::Integer) ^ (5000::Int)))
-- when (not$ NoC `elem` opts) $ do
-- putStrLn$ " Comparison to C's rand():"
-- timeit_foreign th freq "ptr store in C loop" store_loop
-- timeit_foreign th freq "rand/store in C loop" blast_rands
-- timeit_foreign th freq "rand in Haskell loop" (\n ptr -> forM_ [1..n]$ \_ -> rand )
-- timeit_foreign th freq "rand/store in Haskell loop" (\n ptr -> forM_ [1..n]$ \_ -> do n <- rand; poke ptr n )
-- return ()
-- Test with 1 thread and numCapabilities threads:
gamut 1
when (numCapabilities > 1) $ do
putStrLn $ "\nNow "++ show numCapabilities ++" threads, reporting mean randoms-per-second-per-thread:"
void $ gamut numCapabilities
putStrLn "Finished."
|