wireform-core-0.2.0.0: bench/ParserBench.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Main where
import Criterion.Main
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.ByteString.Builder qualified as BSB
import Data.ByteString.Char8 qualified as BSC
import Data.ByteString.Lazy qualified as LBS
import Data.Int
import Data.Word
import FPBasic qualified
import FlatParse.Basic qualified as FP
import WFBasic qualified
import Wireform.Parser qualified as W
import Wireform.Parser.Driver qualified as W
import Wireform.Parser.Internal (Pure)
------------------------------------------------------------------------
-- Input generation
------------------------------------------------------------------------
-- | N copies of a 4-byte big-endian word
mkWord32Input :: Int -> ByteString
mkWord32Input n =
LBS.toStrict . BSB.toLazyByteString $
mconcat [BSB.word32BE (fromIntegral i) | i <- [0 .. n - 1]]
-- | N copies of a single byte
mkByteInput :: Int -> ByteString
mkByteInput n = BS.replicate n 0x42
{- | Length-prefixed messages: 1-byte length + payload
Each message is 8 bytes of payload (length byte = 0x08)
-}
mkLengthPrefixedInput :: Int -> ByteString
mkLengthPrefixedInput n =
LBS.toStrict . BSB.toLazyByteString $
mconcat
[ BSB.word8 8 <> BSB.byteString (BS.replicate 8 (fromIntegral i))
| i <- [0 .. n - 1]
]
-- | ASCII decimal numbers separated by newlines
mkAsciiDecimalInput :: Int -> ByteString
mkAsciiDecimalInput n =
LBS.toStrict . BSB.toLazyByteString $
mconcat [BSB.stringUtf8 (show i) <> BSB.word8 0x0A | i <- [0 .. n - 1]]
-- | Alternating tag bytes: 0x01 or 0x02, each followed by a word32be
mkTaggedInput :: Int -> ByteString
mkTaggedInput n =
LBS.toStrict . BSB.toLazyByteString $
mconcat
[ BSB.word8 (if even i then 0x01 else 0x02)
<> BSB.word32BE (fromIntegral i)
| i <- [0 .. n - 1]
]
-- | UTF-8 text: ASCII (1-byte) characters
mkAsciiTextInput :: Int -> ByteString
mkAsciiTextInput n = BS.replicate n 0x61 -- 'a'
-- | UTF-8 text: 2-byte characters (Latin-1 supplement, e.g. é = C3 A9)
mkUtf8_2byteInput :: Int -> ByteString
mkUtf8_2byteInput n =
LBS.toStrict . BSB.toLazyByteString $
mconcat [BSB.word8 0xC3 <> BSB.word8 0xA9 | _ <- [1 .. n]]
------------------------------------------------------------------------
-- Wireform parsers
------------------------------------------------------------------------
type WP = W.Parser Pure ()
wfWord32s :: Int -> WP ()
wfWord32s 0 = pure ()
wfWord32s n = do
!_ <- W.anyWord32be
wfWord32s (n - 1)
{-# INLINE wfWord32s #-}
wfBytes :: Int -> WP ()
wfBytes 0 = pure ()
wfBytes n = do
!_ <- W.anyWord8
wfBytes (n - 1)
{-# INLINE wfBytes #-}
wfLengthPrefixed :: Int -> WP ()
wfLengthPrefixed 0 = pure ()
wfLengthPrefixed n = do
!len <- W.anyWord8
!_ <- W.takeBs (fromIntegral len)
wfLengthPrefixed (n - 1)
{-# INLINE wfLengthPrefixed #-}
wfAsciiDecimals :: Int -> WP ()
wfAsciiDecimals 0 = pure ()
wfAsciiDecimals n = do
!_ <- W.anyAsciiDecimalWord
W.word8 0x0A
wfAsciiDecimals (n - 1)
{-# INLINE wfAsciiDecimals #-}
wfTagged :: Int -> WP ()
wfTagged 0 = pure ()
wfTagged n = do
(W.word8 0x01 >> W.anyWord32be >> pure ())
W.<|> (W.word8 0x02 >> W.anyWord32be >> pure ())
wfTagged (n - 1)
{-# INLINE wfTagged #-}
wfAsciiChars :: Int -> WP ()
wfAsciiChars 0 = pure ()
wfAsciiChars n = do
W.skipSatisfyAscii (\_ -> True)
wfAsciiChars (n - 1)
{-# INLINE wfAsciiChars #-}
wfUtf8Chars :: Int -> WP ()
wfUtf8Chars 0 = pure ()
wfUtf8Chars n = do
!_ <- W.anyChar
wfUtf8Chars (n - 1)
{-# INLINE wfUtf8Chars #-}
------------------------------------------------------------------------
-- FlatParse parsers
------------------------------------------------------------------------
type FPP = FP.Parser ()
fpWord32s :: Int -> FPP ()
fpWord32s 0 = pure ()
fpWord32s n = do
!_ <- FP.anyWord32be
fpWord32s (n - 1)
{-# INLINE fpWord32s #-}
fpBytes :: Int -> FPP ()
fpBytes 0 = pure ()
fpBytes n = do
!_ <- FP.anyWord8
fpBytes (n - 1)
{-# INLINE fpBytes #-}
fpLengthPrefixed :: Int -> FPP ()
fpLengthPrefixed 0 = pure ()
fpLengthPrefixed n = do
!len <- FP.anyWord8
!_ <- FP.take (fromIntegral len)
fpLengthPrefixed (n - 1)
{-# INLINE fpLengthPrefixed #-}
fpAsciiDecimals :: Int -> FPP ()
fpAsciiDecimals 0 = pure ()
fpAsciiDecimals n = do
!_ <- FP.anyAsciiDecimalWord
FP.word8 0x0A
fpAsciiDecimals (n - 1)
{-# INLINE fpAsciiDecimals #-}
fpTagged :: Int -> FPP ()
fpTagged 0 = pure ()
fpTagged n = do
(FP.word8 0x01 >> FP.anyWord32be >> pure ())
FP.<|> (FP.word8 0x02 >> FP.anyWord32be >> pure ())
fpTagged (n - 1)
{-# INLINE fpTagged #-}
fpAsciiChars :: Int -> FPP ()
fpAsciiChars 0 = pure ()
fpAsciiChars n = do
FP.skipSatisfyAscii (\_ -> True)
fpAsciiChars (n - 1)
{-# INLINE fpAsciiChars #-}
fpUtf8Chars :: Int -> FPP ()
fpUtf8Chars 0 = pure ()
fpUtf8Chars n = do
!_ <- FP.anyChar
fpUtf8Chars (n - 1)
{-# INLINE fpUtf8Chars #-}
------------------------------------------------------------------------
-- Runners
------------------------------------------------------------------------
runWF :: WP a -> ByteString -> a
runWF p bs = case W.parseByteString p bs of
Right a -> a
Left _ -> error "wireform parse failed"
{-# INLINE runWF #-}
runFP :: FPP a -> ByteString -> a
runFP p bs = case FP.runParser p bs of
FP.OK a _ -> a
_ -> error "flatparse parse failed"
{-# INLINE runFP #-}
------------------------------------------------------------------------
-- Benchmark harness
------------------------------------------------------------------------
main :: IO ()
main = do
let !n = 100000
-- Pre-generate inputs
let !byteInput = mkByteInput n
!word32Input = mkWord32Input n
!lpInput = mkLengthPrefixedInput n
!decInput = mkAsciiDecimalInput n
!taggedInput = mkTaggedInput n
!asciiInput = mkAsciiTextInput n
!utf8Input = mkUtf8_2byteInput n
putStrLn $ "Input sizes:"
putStrLn $ " byte: " <> show (BS.length byteInput) <> " bytes"
putStrLn $ " word32: " <> show (BS.length word32Input) <> " bytes"
putStrLn $ " len-pfx: " <> show (BS.length lpInput) <> " bytes"
putStrLn $ " decimal: " <> show (BS.length decInput) <> " bytes"
putStrLn $ " tagged: " <> show (BS.length taggedInput) <> " bytes"
putStrLn $ " ascii: " <> show (BS.length asciiInput) <> " bytes"
putStrLn $ " utf8-2b: " <> show (BS.length utf8Input) <> " bytes"
defaultMain
[ bgroup
"anyWord8 x100k"
[ bench "wireform" $ nf (runWF (wfBytes n)) byteInput
, bench "flatparse" $ nf (runFP (fpBytes n)) byteInput
]
, bgroup
"anyWord32be x100k"
[ bench "wireform" $ nf (runWF (wfWord32s n)) word32Input
, bench "flatparse" $ nf (runFP (fpWord32s n)) word32Input
]
, bgroup
"length-prefixed messages x100k"
[ bench "wireform" $ nf (runWF (wfLengthPrefixed n)) lpInput
, bench "flatparse" $ nf (runFP (fpLengthPrefixed n)) lpInput
]
, bgroup
"ASCII decimal + newline x100k"
[ bench "wireform" $ nf (runWF (wfAsciiDecimals n)) decInput
, bench "flatparse" $ nf (runFP (fpAsciiDecimals n)) decInput
]
, bgroup
"tagged alternatives x100k"
[ bench "wireform" $ nf (runWF (wfTagged n)) taggedInput
, bench "flatparse" $ nf (runFP (fpTagged n)) taggedInput
]
, bgroup
"anyCharASCII x100k"
[ bench "wireform" $ nf (runWF (wfAsciiChars n)) asciiInput
, bench "flatparse" $ nf (runFP (fpAsciiChars n)) asciiInput
]
, bgroup
"anyChar (2-byte UTF-8) x100k"
[ bench "wireform" $ nf (runWF (wfUtf8Chars n)) utf8Input
, bench "flatparse" $ nf (runFP (fpUtf8Chars n)) utf8Input
]
, -- Flatparse-equivalent real-world benchmarks
bgroup
"sexp"
[ bench "wireform" $ whnf WFBasic.runSexp sexpInp
, bench "flatparse" $ whnf FPBasic.runSexp sexpInp
]
, bgroup
"long keyword"
[ bench "wireform" $ whnf WFBasic.runLongws longwsInp
, bench "flatparse" $ whnf FPBasic.runLongws longwsInp
]
, bgroup
"numeral csv"
[ bench "wireform" $ whnf WFBasic.runNumcsv numcsvInp
, bench "flatparse" $ whnf FPBasic.runNumcsv numcsvInp
]
, bgroup
"lambda term"
[ bench "wireform" $ whnf WFBasic.runTm tmInp
, bench "flatparse" $ whnf FPBasic.runTm tmInp
]
]
where
sexpInp = BS.concat $ "(" : replicate 33333 "(foo (foo (foo ((bar baza)))))" <> [")"]
longwsInp = BS.concat $ replicate 55555 "thisisalongkeyword "
numcsvInp = BSC.pack (concat ("0" : [", " <> show i | i <- [1 .. 100000 :: Int]]))
tmInp =
BSC.pack
( unlines
( [ "let x" <> show x <> " = fun f. fun g. fun x. fun y. f (f (f ((g x y + g x y) * g x y * g x y * 13500)));"
| x <- [0 .. 3000 :: Int]
]
<> [("x1000" :: String)]
)
)