pure-noise-0.2.2.0: bench/fnl-compare/FnlBench.hs
-- | Performance comparison against the vendored FastNoiseLite
--
-- Ratios are printed by tasty-bench's 'bcompare': each @pure-noise@ line ends
-- with e.g. @1.05x@ = its mean time relative to the matching @fnl@ line
-- (lower is better; 1.05x means pure-noise is 5% slower).
--
-- Ratios only print when the matching fnl benchmark runs in the same invocation,
-- so filter by group (e.g. @-p \/perlin\/@), not by side.
module Main (main) where
import Control.Monad.State.Strict (evalState, state)
import Data.Vector.Storable qualified as S
import Foreign.C.Types (CFloat (..), CInt (..))
import Foreign.Ptr (Ptr)
import Numeric.Noise
import System.Random.SplitMix (SMGen, mkSMGen, nextFloat)
import Test.Tasty.Bench
import Test.Tasty.Patterns.Printer (printAwkExpr)
main :: IO ()
main =
defaultMain (map (mapLeafBenchmarks addCompare) benchmarks)
-- | Attach a bcompare ratio to every pure-noise leaf, targeting the fnl leaf
-- in the same group (the pattern matches exactly one benchmark).
addCompare :: [String] -> Benchmark -> Benchmark
addCompare (leaf : path) b
| leaf == pnLeaf = bcompare (printAwkExpr (locateBenchmark (fnlLeaf : path))) b
addCompare _ b = b
fnlLeaf, pnLeaf :: String
fnlLeaf = "fnl x" <> show samples
pnLeaf = "pure-noise x" <> show samples
benchmarks :: [Benchmark]
benchmarks =
[ env mkEnv2 $ \ ~(grid, rand, kOne, kSmall) ->
bgroup
"2D"
[ algo2 grid rand kOne kSmall "value" fnlValue value2
, algo2 grid rand kOne kSmall "valueCubic" fnlValueCubic valueCubic2
, algo2 grid rand kOne kSmall "perlin" fnlPerlin perlin2
, algo2 grid rand kOne kSmall "openSimplex2" fnlOpenSimplex2 openSimplex2
, algo2 grid rand kOne kSmall "superSimplex2" fnlOpenSimplex2S superSimplex2
, algo2 grid rand kOne kSmall "cellular" fnlCellular (cellular2 benchCellularConfig)
]
, env mkEnv3 $ \ ~(grid, rand, kOne, kSmall) ->
bgroup
"3D"
[ algo3 grid rand kOne kSmall "value" fnlValue value3
, algo3 grid rand kOne kSmall "valueCubic" fnlValueCubic valueCubic3
, algo3 grid rand kOne kSmall "perlin" fnlPerlin perlin3
, algo3 grid rand kOne kSmall "openSimplex2" fnlOpenSimplex2 openSimplex3
, algo3 grid rand kOne kSmall "superSimplex2" fnlOpenSimplex2S superSimplex3
, algo3 grid rand kOne kSmall "cellular" fnlCellular (cellular3 benchCellularConfig)
]
]
algo2
:: (S.Vector Float, S.Vector Float)
-> (S.Vector Float, S.Vector Float)
-> Float
-> Float
-> String
-> CInt
-> Noise2 Float
-> Benchmark
algo2 grid rand kOne kSmall name ty nz =
bgroup
name
[ bgroup "grid" (pair grid kOne)
, bgroup "freq001" (pair grid kSmall)
, bgroup "random" (pair rand kOne)
]
where
pair ~(xs, ys) k =
[ bench fnlLeaf $ whnfIO (fnlSum2 ty k xs ys)
, bench pnLeaf $ whnf (\kk -> sumNoise2 nz kk xs ys) k
]
{-# INLINE algo2 #-}
algo3
:: (S.Vector Float, S.Vector Float, S.Vector Float)
-> (S.Vector Float, S.Vector Float, S.Vector Float)
-> Float
-> Float
-> String
-> CInt
-> Noise3 Float
-> Benchmark
algo3 grid rand kOne kSmall name ty nz =
bgroup
name
[ bgroup "grid" (pair grid kOne)
, bgroup "freq001" (pair grid kSmall)
, bgroup "random" (pair rand kOne)
]
where
-- lazy pattern: see algo2
pair ~(xs, ys, zs) k =
[ bench fnlLeaf $ whnfIO (fnlSum3 ty k xs ys zs)
, bench pnLeaf $ whnf (\kk -> sumNoise3 nz kk xs ys zs) k
]
{-# INLINE algo3 #-}
----------------------------------------------------------------------------------------------------
-- Matched sweeps
----------------------------------------------------------------------------------------------------
sumNoise2 :: Noise2 Float -> Float -> S.Vector Float -> S.Vector Float -> Float
sumNoise2 nz k xs ys = go 0 0
where
!n = S.length xs
go !i !acc
| i >= n = acc
| otherwise =
go (i + 1) (acc + noise2At nz benchSeed (k * S.unsafeIndex xs i) (k * S.unsafeIndex ys i))
{-# INLINE sumNoise2 #-}
sumNoise3 :: Noise3 Float -> Float -> S.Vector Float -> S.Vector Float -> S.Vector Float -> Float
sumNoise3 nz k xs ys zs = go 0 0
where
!n = S.length xs
go !i !acc
| i >= n = acc
| otherwise =
go
(i + 1)
(acc + noise3At nz benchSeed (k * S.unsafeIndex xs i) (k * S.unsafeIndex ys i) (k * S.unsafeIndex zs i))
{-# INLINE sumNoise3 #-}
----------------------------------------------------------------------------------------------------
-- FFI (fnl_bench_shim.cpp)
----------------------------------------------------------------------------------------------------
foreign import ccall unsafe "fnl_bench_sum2"
c_fnlSum2 :: CInt -> CInt -> CFloat -> CInt -> Ptr Float -> Ptr Float -> IO CFloat
foreign import ccall unsafe "fnl_bench_sum3"
c_fnlSum3 :: CInt -> CInt -> CFloat -> CInt -> Ptr Float -> Ptr Float -> Ptr Float -> IO CFloat
fnlSum2 :: CInt -> Float -> S.Vector Float -> S.Vector Float -> IO Float
fnlSum2 ty k xs ys =
S.unsafeWith xs $ \px ->
S.unsafeWith ys $ \py -> do
CFloat r <- c_fnlSum2 ty fnlBenchSeed (CFloat k) (fromIntegral (S.length xs)) px py
pure r
fnlSum3 :: CInt -> Float -> S.Vector Float -> S.Vector Float -> S.Vector Float -> IO Float
fnlSum3 ty k xs ys zs =
S.unsafeWith xs $ \px ->
S.unsafeWith ys $ \py ->
S.unsafeWith zs $ \pz -> do
CFloat r <- c_fnlSum3 ty fnlBenchSeed (CFloat k) (fromIntegral (S.length xs)) px py pz
pure r
fnlOpenSimplex2, fnlOpenSimplex2S, fnlCellular, fnlPerlin, fnlValueCubic, fnlValue :: CInt
fnlOpenSimplex2 = 0
fnlOpenSimplex2S = 1
fnlCellular = 2
fnlPerlin = 3
fnlValueCubic = 4
fnlValue = 5
-- 1337 is the FNL bench convention.
benchSeed :: Seed
benchSeed = 1337
fnlBenchSeed :: CInt
fnlBenchSeed = 1337
benchCellularConfig :: CellularConfig Float
benchCellularConfig = defaultCellularConfig{cellularDistanceFn = DistEuclidean, cellularResult = Distance}
-- ---------------------------------------------------------------------------
-- Coordinate environments (built once per dimension)
-- ---------------------------------------------------------------------------
-- n = 262144 everywhere: 512*512 (2D grid), 64^3 (3D grid), and the random
-- streams, so every leaf carries the same "xN" count for vps.py.
gridSize2, gridSize3, samples :: Int
gridSize2 = 512
gridSize3 = 64
samples = gridSize2 * gridSize2
-- (grid xs/ys, random xs/ys, kOne = 1.0, kSmall = 0.01). The ks come out of
-- the IO action so the simplifier cannot constant-fold the per-point multiply
-- on the Haskell side only.
mkEnv2 :: IO ((S.Vector Float, S.Vector Float), (S.Vector Float, S.Vector Float), Float, Float)
mkEnv2 = pure (mkGrid2, mkRand2 samples, 1.0, 0.01)
mkEnv3
:: IO
( (S.Vector Float, S.Vector Float, S.Vector Float)
, (S.Vector Float, S.Vector Float, S.Vector Float)
, Float
, Float
)
mkEnv3 = pure (mkGrid3, mkRand3 samples, 1.0, 0.01)
-- Integer lattice in FNL benchmark loop order (x fastest, then y, then z).
mkGrid2 :: (S.Vector Float, S.Vector Float)
mkGrid2 =
( S.generate samples (\i -> fromIntegral (i `mod` gridSize2))
, S.generate samples (\i -> fromIntegral (i `div` gridSize2))
)
mkGrid3 :: (S.Vector Float, S.Vector Float, S.Vector Float)
mkGrid3 =
( S.generate samples (\i -> fromIntegral (i `mod` gridSize3))
, S.generate samples (\i -> fromIntegral ((i `div` gridSize3) `mod` gridSize3))
, S.generate samples (\i -> fromIntegral (i `div` (gridSize3 * gridSize3)))
)
smgen :: SMGen
smgen = mkSMGen 0x9E3779B97F4A7C15
draws :: Int -> S.Vector Float
draws n = S.map (* 512) . evalState (S.generateM n . const . state $ nextFloat) $ smgen
mkRand2 :: Int -> (S.Vector Float, S.Vector Float)
mkRand2 n =
let v = draws (2 * n)
in ( S.generate n (\i -> v S.! (2 * i))
, S.generate n (\i -> v S.! (2 * i + 1))
)
mkRand3 :: Int -> (S.Vector Float, S.Vector Float, S.Vector Float)
mkRand3 n =
let v = draws (3 * n)
in ( S.generate n (\i -> v S.! (3 * i))
, S.generate n (\i -> v S.! (3 * i + 1))
, S.generate n (\i -> v S.! (3 * i + 2))
)