packages feed

streamly-0.10.0: benchmark/Streamly/Benchmark/Data/Serialize.hs

{-# LANGUAGE TemplateHaskell #-}

#undef FUSION_CHECK
#ifdef FUSION_CHECK
{-# OPTIONS_GHC -ddump-simpl -ddump-to-file -dsuppress-all #-}
#endif

-- |
-- Module      : Streamly.Benchmark.Data.Serialize
-- Copyright   : (c) 2023 Composewell
-- License     : BSD-3-Clause
-- Maintainer  : streamly@composewell.com

module Main (main) where

-------------------------------------------------------------------------------
-- Imports
-------------------------------------------------------------------------------

import Control.DeepSeq (NFData(..), deepseq)
import GHC.Generics (Generic)
import System.Random (randomRIO)
#ifndef USE_UNBOX
import Test.QuickCheck (Arbitrary, arbitrary)
#endif

import Streamly.Internal.Data.MutByteArray
import qualified Streamly.Internal.Data.MutByteArray as MBA
#ifdef USE_UNBOX
import Data.Proxy (Proxy(..))
#else
import Control.DeepSeq (force)
import Test.QuickCheck (oneof, generate)
#endif

import Test.Tasty.Bench
import Streamly.Benchmark.Common

#ifndef USE_UNBOX
import Streamly.Benchmark.Data.Serialize.RecCompatible
import Streamly.Benchmark.Data.Serialize.RecNonCompatible
#endif

#ifdef USE_UNBOX
#define SERIALIZE_CLASS Unbox
#define DERIVE_CLASS(typ) $(deriveUnbox [d|instance Unbox typ|])
#define SERIALIZE_OP pokeAt
#define DESERIALIZE_OP peekAt
#else
#define SERIALIZE_CLASS Serialize
#define DERIVE_CLASS(typ) $(deriveSerialize [d|instance Serialize typ|])
#define SERIALIZE_OP serializeAt
#define DESERIALIZE_OP deserializeAt
#endif

-------------------------------------------------------------------------------
-- Types
-------------------------------------------------------------------------------

data Unit = Unit
    deriving (Generic, Show, Eq)

#ifndef USE_TH
instance SERIALIZE_CLASS Unit
#else
DERIVE_CLASS(Unit)
#endif

instance NFData Unit where
    {-# INLINE rnf #-}
    rnf _ = ()

data Sum2
    = Sum21
    | Sum22
    deriving (Generic, Show, Eq)

#ifndef USE_TH
instance SERIALIZE_CLASS Sum2
#else
DERIVE_CLASS(Sum2)
#endif

instance NFData Sum2 where
    {-# INLINE rnf #-}
    rnf _ = ()

data Sum25
    = Sum251
    | Sum252
    | Sum253
    | Sum254
    | Sum255
    | Sum256
    | Sum257
    | Sum258
    | Sum259
    | Sum2510
    | Sum2511
    | Sum2512
    | Sum2513
    | Sum2514
    | Sum2515
    | Sum2516
    | Sum2517
    | Sum2518
    | Sum2519
    | Sum2520
    | Sum2521
    | Sum2522
    | Sum2523
    | Sum2524
    | Sum2525
    deriving (Generic, Show, Eq)

#ifndef USE_TH
instance SERIALIZE_CLASS Sum25
#else
DERIVE_CLASS(Sum25)
#endif

instance NFData Sum25 where
    {-# INLINE rnf #-}
    rnf _ = ()

data Product25
    = Product25
        Int
        Int
        Int
        Int
        Int
        Int
        Int
        Int
        Int
        Int
        Int
        Int
        Int
        Int
        Int
        Int
        Int
        Int
        Int
        Int
        Int
        Int
        Int
        Int
        Int
    deriving (Generic, Show)

#ifndef USE_TH
instance SERIALIZE_CLASS Product25
#else
DERIVE_CLASS(Product25)
#endif

instance NFData Product25 where
    {-# INLINE rnf #-}
    rnf
        (Product25 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19 a20 a21 a22 a23 a24 a25)
           =   a1
            `seq` a2
            `seq` a3
            `seq` a4
            `seq` a5
            `seq` a6
            `seq` a7
            `seq` a8
            `seq` a9
            `seq` a10
            `seq` a11
            `seq` a12
            `seq` a13
            `seq` a14
            `seq` a15
            `seq` a16
            `seq` a17
            `seq` a18
            `seq` a19
            `seq` a20
            `seq` a21
            `seq` a22
            `seq` a23
            `seq` a24
            `seq` a25
            `seq` ()

-- XXX derived Eq instance is not inlined
instance Eq Product25 where
    {-# INLINE (==) #-}
    (==)
        (Product25 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19 a20 a21 a22 a23 a24 a25)
        (Product25 b1 b2 b3 b4 b5 b6 b7 b8 b9 b10 b11 b12 b13 b14 b15 b16 b17 b18 b19 b20 b21 b22 b23 b24 b25)
           =   a1  == b1
            && a2  == b2
            && a3  == b3
            && a4  == b4
            && a5  == b5
            && a6  == b6
            && a7  == b7
            && a8  == b8
            && a9  == b9
            && a10 == b10
            && a11 == b11
            && a12 == b12
            && a13 == b13
            && a14 == b14
            && a15 == b15
            && a16 == b16
            && a17 == b17
            && a18 == b18
            && a19 == b19
            && a20 == b20
            && a21 == b21
            && a22 == b22
            && a23 == b23
            && a24 == b24
            && a25 == b25

-------------------------------------------------------------------------------
-- Simple non-recursive ADT
-------------------------------------------------------------------------------

{-
data CustomDT1
    = CDT1C1
    | CDT1C2 Int
    | CDT1C3 Int Int
    deriving (Generic, Show, Eq)

#ifndef USE_TH
instance SERIALIZE_CLASS CustomDT1
#else
DERIVE_CLASS(CustomDT1)
#endif

instance NFData CustomDT1 where
    {-# INLINE rnf #-}
    rnf CDT1C1 = ()
    rnf (CDT1C2 i) = i `seq` ()
    rnf (CDT1C3 i1 i2) = i1 `seq` i2 `seq` ()
-}

-------------------------------------------------------------------------------
-- Recursive ADT
-------------------------------------------------------------------------------

#ifndef USE_UNBOX
data BinTree a
  = Tree (BinTree a) (BinTree a)
  | Leaf a
  deriving (Show, Read, Eq, Generic)

#ifndef USE_TH
instance Serialize (BinTree a)
#else
$(deriveSerialize [d|instance Serialize a => Serialize (BinTree a)|])
#endif

instance NFData a => NFData (BinTree a) where
  {-# INLINE rnf #-}
  rnf (Leaf a) = rnf a `seq` ()
  rnf (Tree l r) = rnf l `seq` rnf r `seq` ()

instance Arbitrary a => Arbitrary (BinTree a) where
  arbitrary = oneof [Leaf <$> arbitrary, Tree <$> arbitrary <*> arbitrary]

mkBinTree :: (Arbitrary a) => Int -> IO (BinTree a)
mkBinTree = go (generate $ arbitrary)

    where

    go r 0 = Leaf <$> r
    go r n = Tree <$> go r (n - 1) <*> go r (n - 1)

#endif

-------------------------------------------------------------------------------
-- Size helpers
-------------------------------------------------------------------------------

{-# INLINE getSize #-}
getSize :: forall a. SERIALIZE_CLASS a => a -> Int
#ifdef USE_UNBOX
getSize _ = sizeOf (Proxy :: Proxy a)
#else
getSize = addSizeTo 0
#endif

-------------------------------------------------------------------------------
-- Common helpers
-------------------------------------------------------------------------------

-- Parts of "f" that are dependent on val will not be optimized out.
{-# INLINE loop #-}
loop :: Int -> (a -> IO b) -> a -> IO ()
loop count f val = go count val
    where

    go n x = do
        if n > 0
        then f x >> go (n-1) x
        else return ()

-- The first arg of "f" is the environment which is not threaded around in the
-- loop.
{-# INLINE loopWith #-}
loopWith :: Int -> (e -> a -> IO b) -> e -> a -> IO ()
loopWith count f e val = go count val
    where

    go n x = do
        if n > 0
        then f e x >> go (n-1) x
        else return ()

benchSink :: NFData b => String -> Int -> (Int -> IO b) -> Benchmark
benchSink name times f = bench name (nfIO (randomRIO (times, times) >>= f))

-------------------------------------------------------------------------------
-- Serialization Helpers
-------------------------------------------------------------------------------

{-
{-# INLINE pokeWithSize #-}
pokeWithSize :: SERIALIZE_CLASS a => MutByteArray -> a -> IO ()
pokeWithSize arr val = do
    let n = getSize val
    n `seq` SERIALIZE_OP 0 arr val >> return ()

{-# INLINE pokeTimesWithSize #-}
pokeTimesWithSize :: SERIALIZE_CLASS a => a -> Int -> IO ()
pokeTimesWithSize val times = do
    let n = getSize val
    arr <- MBA.new n
    loopWith times pokeWithSize arr val
-}

{-# INLINE poke #-}
poke :: SERIALIZE_CLASS a => MutByteArray -> a -> IO ()
poke arr val = SERIALIZE_OP 0 arr val >> return ()

{-# INLINE pokeTimes #-}
pokeTimes :: SERIALIZE_CLASS a => a -> Int -> IO ()
pokeTimes val times = do
    let n = getSize val
    arr <- MBA.new n
    loopWith times poke arr val

{-# INLINE encode #-}
encode :: SERIALIZE_CLASS a => a -> IO ()
encode val = do
    let n = getSize val
    arr <- MBA.new n
    SERIALIZE_OP 0 arr val >> return ()

{-# INLINE encodeTimes #-}
encodeTimes :: SERIALIZE_CLASS a => a -> Int -> IO ()
encodeTimes val times = loop times encode val

{-# INLINE peek #-}
peek :: forall a. (NFData a, SERIALIZE_CLASS a) =>
    (a, Int) -> MutByteArray -> IO ()
#ifdef USE_UNBOX
peek (_val, _) arr = do
        (val1 :: a) <- DESERIALIZE_OP 0 arr
#else
peek (_val, n) arr = do
        (_, val1 :: a) <- DESERIALIZE_OP 0 arr n
#endif
        -- If the datatype is not deeply strict or deepseq is not used then use
        -- Equality.
        -- Ensure that we are actually constructing the type and using it. This
        -- is important, otherwise the structure is created and discarded, the
        -- cost of creation of the structure is not accounted. Otherwise we may
        -- just read the values and discard them. The comparison adds to the
        -- cost though. We could use deepseq but then we need to write
        -- instances of NFData and ensure that they are correct and perform
        -- well. Equality check also ensures correctness.
        {-
        if (val1 /= val)
        then error "peek: no match"
        else return ()
        -}
        val1 `deepseq` return ()

{-# INLINE peekTimes #-}
peekTimes :: (NFData a, SERIALIZE_CLASS a) => Int -> a -> Int -> IO ()
peekTimes n val times = do
    arr <- MBA.new n
    _ <- SERIALIZE_OP 0 arr val
    loopWith times peek (val, n) arr

{-# INLINE trip #-}
trip :: forall a. (NFData a, SERIALIZE_CLASS a) => a -> IO ()
trip val = do
    let n = getSize val
    arr <- MBA.new n
    _ <- SERIALIZE_OP 0 arr val
#ifdef USE_UNBOX
    (val1 :: a) <- DESERIALIZE_OP 0 arr
#else
    (_, val1 :: a) <- DESERIALIZE_OP 0 arr n
#endif
    -- Do not remove this or use deepseq, see the comments in peek.
    {-
    if (val1 /= val)
    then error "roundtrip: no match"
    else return ()
    -}
    -- Note: deepseq is not needed if the datatype is strict
    val1 `deepseq` return ()

{-# INLINE roundtrip #-}
roundtrip :: (NFData a, SERIALIZE_CLASS a) => a -> Int -> IO ()
roundtrip val times = loop times trip val

-------------------------------------------------------------------------------
-- Benchmarks
-------------------------------------------------------------------------------

{-# INLINE benchConst #-}
benchConst ::
       String
    -> (forall a. (NFData a, SERIALIZE_CLASS a) =>
        Int -> a -> Int -> IO ())
    -> Int
    -> Benchmark
benchConst gname f times =
    bgroup gname
       [ let !n = getSize Unit
          in benchSink "Unit" times (f n Unit)
       {-
       , let !n = getSize CDT1C1
          in benchSink "C1" times (f n CDT1C1)
       , let val = CDT1C2 5
             !n = getSize val
          in benchSink "C2" (times `div` 2) (f n val)
       , let val = CDT1C3 5 2
             !n = getSize val
          in benchSink "C3" (times `div` 3) (f n val)
       -}
       , let !n = getSize Sum21
          in benchSink "Sum2" times (f n Sum21)
       , let !n = getSize Sum2525
          in benchSink "Sum25" times (f n Sum2525)
       , let val = Product25 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
             !n = getSize val
          in benchSink "Product25" (times `div` 26) (f n val)
        ]

#ifndef USE_UNBOX
{-# INLINE benchVar #-}
benchVar ::
       String
    -> (forall a. (NFData a, SERIALIZE_CLASS a) =>
        Int -> a -> Int -> IO ())
    -> BinTree Int
    -> [Int]
    -> Int
    -> Benchmark
benchVar gname f tInt lInt times =
    bgroup gname
       [ let !n = getSize tInt
           in benchSink "bintree-int" times (f n tInt)
        , let !n = getSize lInt
           in benchSink "list-int" times (f n lInt)
        ]

{-
{-# INLINE benchGeneric #-}
benchGeneric
    :: (NFData b, Serialize b)
    => String
    -> String
    -> (forall a. (NFData a, Serialize a) =>
        Int -> a -> Int -> IO ())
    -> b
    -> Int
    -> Benchmark
benchGeneric gname valId f val times =
    bgroup gname
       [ let !n = getSize val
          in benchSink valId times (f n val)
       ]
-}

{-# INLINE benchLargeRec #-}
benchLargeRec ::
        String
    -> (forall a. (NFData a, Serialize a) =>
        Int -> a -> Int -> IO ())
    -> RecNonCompatible
    -> RecCompatible
    -> Int
    -> Benchmark
benchLargeRec gname f recL recR times =
    bgroup gname
       [ let !n = getSize recL
           in benchSink "RecNonCompatible" times (f n recL)
       ,  let !n = getSize recR
           in benchSink "RecCompatible" times (f n recR)
       ]
#endif

-- Times is scaled by the number of constructors to normalize
#ifdef USE_UNBOX
allBenchmarks :: Int -> [Benchmark]
allBenchmarks times =
#else
allBenchmarks ::
       BinTree Int -> [Int] -> RecNonCompatible -> RecCompatible -> Int -> [Benchmark]
allBenchmarks tInt lInt recL recR times =
#endif
    [ bgroup "sizeOf"
        [
#ifndef USE_UNBOX
          bench "bintree-int" $ nf getSize tInt
        , bench "list-int" $ nf getSize lInt
#endif
        ]
    , benchConst "poke" (const pokeTimes) times
    -- , benchConst "pokeWithSize" (const pokeTimesWithSize) times
    , benchConst "encode" (const encodeTimes) times
    , benchConst "peek" peekTimes times
    , benchConst "roundtrip" (const roundtrip) times
#ifndef USE_UNBOX
    , benchVar "poke" (const pokeTimes) tInt lInt 1
    -- , benchVar "pokeWithSize" (const pokeTimesWithSize) tInt lInt 1
    , benchVar "encode" (const encodeTimes) tInt lInt 1
    , benchVar "peek" peekTimes tInt lInt 1
    , benchVar "roundtrip" (const roundtrip) tInt lInt 1
    , benchLargeRec "poke" (const pokeTimes) recL recR times
    , benchLargeRec "encode" (const encodeTimes) recL recR times
    , benchLargeRec "peek" peekTimes recL recR times
    , benchLargeRec "roundtrip" (const roundtrip) recL recR times
#endif
    ]

-------------------------------------------------------------------------------
-- Driver
-------------------------------------------------------------------------------

main :: IO ()
main = do
#ifndef USE_UNBOX
    -- Approximately 100000 constructors
    -- Assuming Leaf nodes constitute two constructors (Leaf, Int) and internal
    -- nodes 1 level = log_2 (100001/3) + 1 = 16
    !(tInt :: BinTree Int) <- force <$> mkBinTree 16

    let !recL = valRecNonCompatible
    let !recR = valRecCompatible

    -- Approximately 100000 constructors, assuming two constructors (Cons, Int)
    -- per element.
    let lInt = [1..50000 :: Int]
    let !len = length lInt -- evaluate the list
#endif
#ifndef FUSION_CHECK
    -- This can take too much memory/CPU, need to restrict the test
    -- runQC
#ifdef USE_UNBOX
    runWithCLIOpts defaultStreamSize allBenchmarks
#else
    len
        `seq` runWithCLIOpts
                  defaultStreamSize
                  (allBenchmarks tInt lInt recL recR)

#endif
#else
    -- Enable FUSION_CHECK macro at the beginning of the file
    -- Enable one benchmark below, and run the benchmark
    -- Check the .dump-simpl output
    let value = 100000
    -- print $ getSize (CDT1C3 4 2)
    -- print $ getSize tInt
    -- print $ getSize lInt

    -- pokeTimes tInt 1

    -- peekTimes ((CDT1C2 (5 :: Int)) :: CustomDT1) value
    -- peekTimes (Sum2525) value
    -- peekTimes (Product25 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) value
    -- peekTimes tInt 1
    let !n = getSize lInt
    peekTimes n lInt 1

    -- roundtrip ((CDT1C2 (5 :: Int)) :: CustomDT1) value
    return ()
#endif