wireform-parquet-0.1.0.0: bench/DictionaryThroughput.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ImportQualifiedPost #-}
module Main (main) where
import Control.DeepSeq (NFData (..))
import Control.Exception (evaluate)
import Control.Monad (unless)
import Criterion.Main (bench, bgroup, defaultMain, env, whnf)
import Data.Bits (shiftR, (.&.), (.|.))
import Data.ByteString qualified as BS
import Data.ByteString.Internal qualified as BSI
import Data.Primitive.Types (Prim)
import Data.Vector qualified as V
import Data.Vector.Primitive qualified as P
import Data.Vector.Primitive.Mutable qualified as M
import Data.Word (Word32, Word64, Word8)
import Foreign.Marshal.Utils (fillBytes)
import Foreign.Ptr (Ptr)
import Foreign.Storable (pokeByteOff)
import GHC.Float (castWord32ToFloat, castWord64ToDouble)
import Parquet.Write qualified as W
import System.Mem (performMajorGC)
rowCount :: Int
rowCount = 8192
data Input = Input
{ inputName :: !String
, inputColumn :: !W.ColumnData
, inputUniques :: !W.ColumnData
, inputIndices :: !(P.Vector Int)
}
data Spacing = Dense | Sparse
data Kind
= Int32Keys !Spacing
| Int64Keys !Spacing
| FloatKeys !Spacing
| DoubleKeys !Spacing
| BinaryKeys {-# UNPACK #-} !Int
| ShortTextKeys
| CommonEndKeys
| MultipleMiddleKeys
data InputSpec = InputSpec
{ specName :: !String
, specCardinality :: {-# UNPACK #-} !Int
, specKind :: !Kind
}
-- Recipes contain no input buffers. Criterion keeps only its current
-- fixture alive, matching the reference harness's per-case lifetime.
inputSpecs :: [InputSpec]
inputSpecs = concatMap shapes [8, 1024, rowCount]
where
shapes cardinality =
let sample label kind = InputSpec (label ++ " " ++ show cardinality ++ " unique") cardinality kind
in [ sample "INT32" (Int32Keys Dense)
, sample "INT64" (Int64Keys Dense)
, sample "FLOAT" (FloatKeys Dense)
, sample "DOUBLE" (DoubleKeys Dense)
, sample "BYTE_ARRAY" (BinaryKeys 8)
, sample "INT32 sparse" (Int32Keys Sparse)
, sample "INT64 sparse" (Int64Keys Sparse)
, sample "FLOAT sparse" (FloatKeys Sparse)
, sample "DOUBLE sparse" (DoubleKeys Sparse)
, sample "BYTE_ARRAY 64 bytes" (BinaryKeys 64)
, sample "BYTE_ARRAY 4096 bytes" (BinaryKeys 4096)
, sample "BYTE_ARRAY common ends 64 bytes" CommonEndKeys
, sample "BYTE_ARRAY short text" ShortTextKeys
, sample "BYTE_ARRAY common ends multiple words 64 bytes" MultipleMiddleKeys
]
-- Explicit IO allocation prevents a recipe from retaining a pure vector
-- shared between its preflight and later Criterion environment.
newPrimitiveVector :: (Prim a) => (Int -> a) -> IO (P.Vector a)
newPrimitiveVector valueAt = do
values <- M.unsafeNew rowCount
let fill !row
| row == rowCount = P.unsafeFreeze values
| otherwise = M.unsafeWrite values row (valueAt row) >> fill (row + 1)
fill 0
spread32 :: Int -> Word32
spread32 key = fromIntegral key * 1664525 + 1013904223
spread64 :: Int -> Word64
spread64 key = fromIntegral key * 6364136223846793005 + 1442695040888963407
putWordLE :: Ptr Word8 -> Int -> Word64 -> IO ()
putWordLE ptr offset value = fill 0
where
fill !byte
| byte == 8 = pure ()
| otherwise = do
pokeByteOff ptr (offset + byte) (fromIntegral (value `shiftR` (byte * 8)) :: Word8)
fill (byte + 1)
newByteValue :: Kind -> Word64 -> IO BS.ByteString
newByteValue kind key = case kind of
BinaryKeys width -> filled width (\ptr -> putWordLE ptr 0 key)
CommonEndKeys -> filled 64 (\ptr -> putWordLE ptr 24 key)
MultipleMiddleKeys ->
let (high, low) = key `quotRem` 4
in filled 64 (\ptr -> putWordLE ptr 16 low >> putWordLE ptr 24 high)
ShortTextKeys -> do
let digits !count !value
| value < 10 = count
| otherwise = digits (count + 1) (value `quot` 10)
count = digits 1 key
BSI.create count $ \ptr ->
let writeDigit !offset !value = do
let (remaining, digit) = value `quotRem` 10
pokeByteOff ptr offset (48 + fromIntegral digit :: Word8)
if offset == 0 then pure () else writeDigit (offset - 1) remaining
in writeDigit (count - 1) key
_ -> error "numeric recipe passed to newByteValue"
where
filled size fields = BSI.create size $ \ptr -> fillBytes ptr 97 size >> fields ptr
newInput :: InputSpec -> IO Input
newInput spec = do
let cardinality = specCardinality spec
keyAt row = (row * 104729) `rem` cardinality
indices <- newPrimitiveVector (`rem` cardinality)
column <- case specKind spec of
Int32Keys spacing -> W.ColInt32 <$> newPrimitiveVector (\row -> case spacing of Dense -> fromIntegral (keyAt row); Sparse -> fromIntegral (spread32 (keyAt row)))
Int64Keys spacing -> W.ColInt64 <$> newPrimitiveVector (\row -> case spacing of Dense -> fromIntegral (keyAt row); Sparse -> fromIntegral (spread64 (keyAt row)))
FloatKeys spacing -> W.ColFloat <$> newPrimitiveVector (\row -> castWord32ToFloat (0x3f000000 .|. case spacing of Dense -> fromIntegral (keyAt row); Sparse -> spread32 (keyAt row) .&. 0x007fffff))
DoubleKeys spacing -> W.ColDouble <$> newPrimitiveVector (\row -> castWord64ToDouble (0x3fe0000000000000 .|. case spacing of Dense -> fromIntegral (keyAt row); Sparse -> spread64 (keyAt row) .&. 0x000fffffffffffff))
kind -> W.ColByteArray <$> V.generateM rowCount (\row -> newByteValue kind (fromIntegral (keyAt row)))
pure (Input (specName spec) column (takeColumn cardinality column) indices)
takeColumn :: Int -> W.ColumnData -> W.ColumnData
takeColumn count column = case column of
W.ColInt32 values -> W.ColInt32 (P.take count values)
W.ColInt64 values -> W.ColInt64 (P.take count values)
W.ColFloat values -> W.ColFloat (P.take count values)
W.ColDouble values -> W.ColDouble (P.take count values)
W.ColBool values -> W.ColBool (V.take count values)
W.ColByteArray values -> W.ColByteArray (V.take count values)
forceColumn :: W.ColumnData -> ()
forceColumn column = case column of
W.ColByteArray values -> V.foldl' (\count value -> count + BS.length value) 0 values `seq` ()
W.ColBool values -> V.foldl' (\count value -> if value then count + 1 else count) (0 :: Int) values `seq` ()
_ -> W.columnDataLength column `seq` ()
validateInput :: Input -> IO ()
validateInput sample = do
_ <- evaluate (forceColumn (inputColumn sample))
let dictionary = W.buildDictionary (inputColumn sample)
unless
(W.dictUniques dictionary == inputUniques sample && W.dictIndices dictionary == P.map fromIntegral (inputIndices sample))
(fail (inputName sample ++ ": dictionary values/first-appearance indices differ"))
-- Return only the source column; expected outputs and preflight results
-- must not become part of the live heap scanned during timed iterations.
newtype PreparedInput = PreparedInput W.ColumnData
instance NFData PreparedInput where
rnf (PreparedInput column) = forceColumn column
prepareInput :: InputSpec -> IO PreparedInput
prepareInput spec = do
performMajorGC
prepared <- do
sample <- newInput spec
validateInput sample
column <- evaluate (inputColumn sample)
pure (PreparedInput column)
_ <- evaluate (rnf prepared)
performMajorGC
pure prepared
main :: IO ()
main = do
-- Validate every recipe even for --list, but discard each input before
-- preparing the next one. Environments allocate fresh timed fixtures.
mapM_ (\spec -> newInput spec >>= validateInput) inputSpecs
performMajorGC
defaultMain
[ bgroup "dictionary build" $
map
(\spec -> env (prepareInput spec) (\(PreparedInput column) -> bench (specName spec) (whnf W.buildDictionary column)))
inputSpecs
]