vinyl-0.9.0: benchmarks/StorableBench.hs
{-# LANGUAGE DataKinds, GADTs, OverloadedLabels, ScopedTypeVariables,
TypeOperators #-}
-- A benchmark where we initialize a 'V.Vector' of random vertices,
-- each carrying 3D position, 2D texture coordinates, and a 3D normal
-- vector. A calculation is carried out where we multiply the y
-- coordinate of each point's normal vector by 2, then sum all normal
-- vector coordinates over all vertices. This calculation is performed
-- by interfacing the vertex data as a flat record, a traditional
-- record of "Linear" finite dimensional vector types, and a vinyl
-- record of linear fields.
import Lens.Micro
import Lens.Micro.Extras (view)
import Control.Monad (when)
import qualified Data.Foldable as F
import qualified Data.Vector.Storable as V
import qualified Data.Vector.Storable.Mutable as VM
import Data.Vinyl
import Foreign.Ptr (castPtr)
import Foreign.Storable (Storable(..))
import Linear (V2, V3, _y)
import System.Random.MWC (withSystemRandom, Variate(..), GenIO)
import Criterion.Main
randVec :: (Storable a, Variate a) => Int -> GenIO -> IO (V.Vector a)
randVec n g = VM.replicateM n (uniform g) >>=
V.unsafeFreeze
randVecStd :: (Storable a, Variate a) => Int -> IO (V.Vector a)
randVecStd = withSystemRandom . randVec
type MyFields a = [ '("pos", V3 a), '("tex", V2 a), '("normal", V3 a) ]
type MyVertex a = FieldRec (MyFields a)
(*~) :: Num a => ASetter s t a a -> a -> s -> t
l *~ x = l %~ (* x)
infixr 4 *~
vinylNormSumLens :: (Num a, Storable a) => V.Vector (MyVertex a) -> a
vinylNormSumLens = V.sum . V.map (F.sum . view (rlensf #normal))
vinylNormSumLabel :: (Num a, Storable a) => V.Vector (MyVertex a) -> a
vinylNormSumLabel = V.sum . V.map (F.sum . rvalf #normal)
doubleNormYLens :: V.Vector (MyVertex Float) -> V.Vector (MyVertex Float)
doubleNormYLens = V.map (rlensf #normal . _y *~ (2::Float))
doubleNormY :: V.Vector (MyVertex Float) -> V.Vector (MyVertex Float)
doubleNormY = V.map (\(p :& t :& Field n :& RNil) ->
p :& t :& Field (_y *~ (2::Float) $ n) :& RNil)
vinylNormSum :: (Num a, Storable a) => V.Vector (MyVertex a) -> a
vinylNormSum = V.sum . V.map (F.sum . (\(_ :& _ :& Field vn :& RNil) -> vn))
main :: IO ()
main = do vals <- randVecStd $ n * 8 :: IO (V.Vector Float)
let vinylVerts = V.unsafeCast vals :: V.Vector (MyVertex Float)
flatVerts = V.unsafeCast vals
reasVerts = V.unsafeCast vals
vinylAns = vinylNormSum $ doubleNormY vinylVerts
vinylLans = vinylNormSumLens $ doubleNormYLens vinylVerts
vinylLabAns = vinylNormSumLabel $ doubleNormYLens vinylVerts
flatAns = flatNormSum $ doubleNormFlat flatVerts
reasAns = reasNormSum $ doubleNormReas reasVerts
when (any (/= vinylAns) [ vinylLans, flatAns, reasAns, vinylLabAns ])
(error "Not all versions compute the same answer")
defaultMain [ bench "flat" $
whnf (flatNormSum . doubleNormFlat) flatVerts
, bench "vinyl" $
whnf (vinylNormSum . doubleNormY) vinylVerts
, bench "vinyl-lens" $
whnf (vinylNormSumLens . doubleNormYLens) vinylVerts
, bench "vinyl-label" $
whnf (vinylNormSumLabel . doubleNormYLens) vinylVerts
, bench "reasonable" $
whnf (reasNormSum . doubleNormReas) reasVerts ]
where n = 1000
--------------------------------------------------------------------------------
-- Baseline data structures for comparison
-- Don't trust data structures at all? Use a flat record where only
-- the field prefixes denote their roles.
data TotallyFlat a = Flat { px :: !a
, py :: !a
, pz :: !a
, tu :: !a
, tv :: !a
, nx :: !a
, ny :: !a
, nz :: !a }
instance Storable a => Storable (TotallyFlat a) where
sizeOf _ = sizeOf (undefined::a) * 8
alignment _ = alignment (undefined::a)
peek ptr = Flat <$> peek ptr' <*> peekElemOff ptr' 1 <*> peekElemOff ptr' 2
<*> peekElemOff ptr' 3 <*> peekElemOff ptr' 4
<*> peekElemOff ptr' 5 <*> peekElemOff ptr' 6
<*> peekElemOff ptr' 7
where ptr' = castPtr ptr
poke ptr (Flat px' py' pz' tu' tv' nx' ny' nz') = do poke ptr' px'
pokeElemOff ptr' 1 py'
pokeElemOff ptr' 2 pz'
pokeElemOff ptr' 3 tu'
pokeElemOff ptr' 4 tv'
pokeElemOff ptr' 5 nx'
pokeElemOff ptr' 6 ny'
pokeElemOff ptr' 7 nz'
where ptr' = castPtr ptr
flatNormSum :: (Num a, Storable a) => V.Vector (TotallyFlat a) -> a
flatNormSum = V.sum . V.map (\v -> nx v + ny v + nz v)
doubleNormFlat :: V.Vector (TotallyFlat Float) -> V.Vector (TotallyFlat Float)
doubleNormFlat = V.map (\v -> v { ny = ny v * 2 })
-- A more reasonable approach to a vertex record.
data Reasonable a = Reasonable { rPos :: V3 a
, rTex :: V2 a
, rNorm :: V3 a }
instance Storable a => Storable (Reasonable a) where
sizeOf _ = sizeOf (undefined::V3 a)*2 + sizeOf (undefined::V2 a)
alignment _ = alignment (undefined::V3 a)
peek ptr = Reasonable <$> peek (castPtr ptr)
<*> peekByteOff (castPtr ptr) szx
<*> peekByteOff (castPtr ptr) (szx + szy)
where szx = sizeOf (undefined::V3 a)
szy = sizeOf (undefined::V2 a)
poke ptr (Reasonable p t n) = do poke (castPtr ptr) p
pokeByteOff (castPtr ptr) szx t
pokeByteOff (castPtr ptr) (szx + szy) n
where szx = sizeOf (undefined::V3 a)
szy = sizeOf (undefined::V2 a)
reasNormSum :: (Num a, Storable a) => V.Vector (Reasonable a) -> a
reasNormSum = V.sum . V.map (F.sum . rNorm)
doubleNormReas :: V.Vector (Reasonable Float) -> V.Vector (Reasonable Float)
doubleNormReas = V.map (\v -> v { rNorm = (_y *~ 2) $ rNorm v })