pure-borrow-0.1.0.0: internal-src/test-bench-common/PureBorrow/Internal/Bench/Unboxed.hs
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE QualifiedDo #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# OPTIONS_GHC -Wno-name-shadowing #-}
module PureBorrow.Internal.Bench.Unboxed (
defaultMain,
benches,
directFixedUnboxedKernel,
directFixedUnboxedMaterialization,
directGrowableUnboxedGrowthKernel,
directGrowableUnboxedMaterialization,
directGrowableUnboxedNoGrowthKernel,
owningBoxedQsortKernel,
pureBorrowFixedUnboxedKernel,
pureBorrowFixedUnboxedMaterialization,
pureBorrowGrowableUnboxedGrowthKernel,
pureBorrowGrowableUnboxedMaterialization,
pureBorrowGrowableUnboxedNoGrowthKernel,
unrestrictedBoxedFftKernel,
unrestrictedBoxedQsortKernel,
unrestrictedUnboxedFftKernel,
unrestrictedUnboxedQsortKernel,
) where
import Control.Concurrent.DivideConquer.Linear qualified as DC
import Control.Functor.Linear qualified as Control
import Control.Monad.Borrow.Pure
import Control.Monad.ST.Strict (ST, runST)
import Control.Syntax.DataFlow qualified as DataFlow
import Data.Complex (Complex (..))
import Data.Ref.Linear qualified as Ref
import Data.STRef (STRef, newSTRef, readSTRef, writeSTRef)
import Data.Vector qualified as B
import Data.Vector.Generic qualified as G
import Data.Vector.Generic.Mutable.Linear.Borrow.Unrestricted qualified as UnrestrictedVector
import Data.Vector.Mutable.Linear.Borrow qualified as OwningVector
import Data.Vector.Unboxed qualified as U
import Data.Vector.Unboxed.Mutable qualified as UM
import Data.Vector.Unboxed.Mutable.Growable.Linear.Borrow qualified as Growable
import Data.Vector.Unboxed.Mutable.Growable.Linear.Borrow.Internal qualified as GrowableInternal
import Data.Vector.Unboxed.Mutable.Linear.Borrow qualified as Vector
import Data.Vector.Unboxed.Mutable.Linear.Borrow.Internal qualified as VectorInternal
import GHC.IO (unsafePerformIO)
import Prelude.Linear
import Test.Tasty.Bench hiding (defaultMain)
import Test.Tasty.Bench qualified as Bench
import Unsafe.Linear qualified as Unsafe
import Prelude qualified as NonLinear
directFixedUnboxedKernel :: U.Vector Int -> U.Vector Int
{-# NOINLINE directFixedUnboxedKernel #-}
directFixedUnboxedKernel input =
U.modify (\vector -> directWorker (UM.length vector) 0 vector) input
directWorker :: Int -> Int -> UM.MVector s Int -> ST s ()
{-# NOINLINE directWorker #-}
directWorker !length_ !index vector
| index >= length_ = NonLinear.pure ()
| otherwise = do
value <- UM.unsafeRead vector index
UM.unsafeWrite vector index (value + 1)
directWorker length_ (index + 1) vector
{- | Benchmark-only monomorphic finalization for the fixed-vector kernels.
For 'Int', 'move' is observationally the identity. Bypassing the public
materialization loop isolates the update worker; this is not an API-performance
root.
-}
unsafeBenchmarkFreezeFixedInt :: Vector.Vector Int %1 -> Ur (U.Vector Int)
{-# NOINLINE unsafeBenchmarkFreezeFixedInt #-}
unsafeBenchmarkFreezeFixedInt =
Unsafe.toLinear \(VectorInternal.Vector vector) ->
Ur (unsafePerformIO (U.unsafeFreeze vector))
directFixedUnboxedMaterialization :: U.Vector Int -> U.Vector Int
{-# NOINLINE directFixedUnboxedMaterialization #-}
directFixedUnboxedMaterialization input =
U.modify (\_ -> NonLinear.pure ()) input
pureBorrowFixedUnboxedMaterialization :: U.Vector Int -> U.Vector Int
{-# NOINLINE pureBorrowFixedUnboxedMaterialization #-}
pureBorrowFixedUnboxedMaterialization input =
unur $
linearly \linear ->
Vector.toVector (Vector.fromVector input linear)
pureBorrowFixedUnboxedKernel :: U.Vector Int -> U.Vector Int
{-# NOINLINE pureBorrowFixedUnboxedKernel #-}
pureBorrowFixedUnboxedKernel input =
unur $ linearly \linear -> DataFlow.do
(ownerLinear, runLinear) <- dup linear
runBO runLinear Control.do
(vector, lend) <- borrowM (Vector.fromVector input ownerLinear)
pureBorrowWorker (U.length input) 0 vector
pureAfter (unsafeBenchmarkFreezeFixedInt (reclaim lend))
directGrowableUnboxedNoGrowthKernel :: U.Vector Int -> U.Vector Int
{-# NOINLINE directGrowableUnboxedNoGrowthKernel #-}
directGrowableUnboxedNoGrowthKernel input =
U.modify
( \vector -> do
-- The header ref mirrors the growable owner's indirection, so the
-- baseline pays for the same loads the measured variant does.
header <- newSTRef (U.length input, vector)
(_, content) <- readSTRef header
directWorker (UM.length content) 0 content
-- Match the growable owner's final header access.
(_, _) <- readSTRef header
NonLinear.pure ()
)
input
{- | Benchmark-only monomorphic finalization for the growable-vector kernels.
The reclaimed owner is consumed exactly once and only its initialized prefix is
frozen. As with 'unsafeBenchmarkFreezeFixedInt', this isolates the kernel and
must not be interpreted as public materialization cost.
-}
unsafeBenchmarkFreezeGrowableInt ::
Growable.GrowableVector Int %1 ->
Ur (U.Vector Int)
{-# NOINLINE unsafeBenchmarkFreezeGrowableInt #-}
unsafeBenchmarkFreezeGrowableInt =
Unsafe.toLinear \(GrowableInternal.GrowableVector ref) ->
case Ref.free ref of
GrowableInternal.Header logicalSize vector ->
Ur
( unsafePerformIO
(U.unsafeFreeze (UM.unsafeTake logicalSize vector))
)
directGrowableUnboxedMaterialization :: U.Vector Int -> U.Vector Int
{-# NOINLINE directGrowableUnboxedMaterialization #-}
directGrowableUnboxedMaterialization input =
U.modify
( \vector -> do
header <- newSTRef (U.length input, vector)
(_, _) <- readSTRef header
NonLinear.pure ()
)
input
pureBorrowGrowableUnboxedMaterialization :: U.Vector Int -> U.Vector Int
{-# NOINLINE pureBorrowGrowableUnboxedMaterialization #-}
pureBorrowGrowableUnboxedMaterialization input =
unur $
linearly \linear ->
Growable.toVector (Growable.fromVector input linear)
pureBorrowGrowableUnboxedNoGrowthKernel :: U.Vector Int -> U.Vector Int
{-# NOINLINE pureBorrowGrowableUnboxedNoGrowthKernel #-}
pureBorrowGrowableUnboxedNoGrowthKernel input =
unur $ linearly \linear -> DataFlow.do
(ownerLinear, runLinear) <- dup linear
runBO runLinear Control.do
(vector, lend) <-
borrowM
( Growable.unsafeFromMutable
(unsafePerformIO (U.thaw input))
ownerLinear
)
vector <-
Growable.withContent_ vector \content ->
pureBorrowWorker (U.length input) 0 content
let !() = consume vector
pureAfter (unsafeBenchmarkFreezeGrowableInt (reclaim lend))
directGrowableUnboxedGrowthKernel :: U.Vector Int -> (U.Vector Int, Int)
{-# NOINLINE directGrowableUnboxedGrowthKernel #-}
-- The result length differs from the input's, so this one cannot go through
-- 'U.modify'; 'runST' keeps it equally safe, and the buffer never escapes.
directGrowableUnboxedGrowthKernel input = runST do
initial <- UM.unsafeNew 0
header <- newSTRef (0, initial)
directGrowthWorker input 0 header
(_, capacityContent) <- readSTRef header
(logicalSize, content) <- readSTRef header
frozen <- U.freeze (UM.unsafeTake logicalSize content)
NonLinear.pure (frozen, UM.length capacityContent)
directGrowthWorker ::
U.Vector Int ->
Int ->
STRef s (Int, UM.MVector s Int) ->
ST s ()
{-# NOINLINE directGrowthWorker #-}
directGrowthWorker input !index header
| index >= U.length input = NonLinear.pure ()
| otherwise = do
(logicalSize, content) <- readSTRef header
grown <-
if logicalSize < UM.length content
then NonLinear.pure content
else do
let !capacity = growthTarget (UM.length content) (logicalSize + 1)
target <- UM.unsafeNew capacity
UM.unsafeCopy
(UM.unsafeTake logicalSize target)
(UM.unsafeTake logicalSize content)
NonLinear.pure target
UM.unsafeWrite grown logicalSize (U.unsafeIndex input index)
writeSTRef header (logicalSize + 1, grown)
directGrowthWorker input (index + 1) header
pureBorrowGrowableUnboxedGrowthKernel :: U.Vector Int -> (U.Vector Int, Int)
{-# NOINLINE pureBorrowGrowableUnboxedGrowthKernel #-}
pureBorrowGrowableUnboxedGrowthKernel input =
unur $ linearly \linear -> DataFlow.do
(ownerLinear, runLinear) <- dup linear
runBO runLinear Control.do
(vector, lend) <- borrowM (Growable.empty ownerLinear)
vector <- pureBorrowGrowthWorker input 0 vector
Growable.capacity vector & \(Ur finalCapacity, vector) -> DataFlow.do
consume vector
pureAfter
(attachCapacity finalCapacity (unsafeBenchmarkFreezeGrowableInt (reclaim lend)))
attachCapacity :: Int -> Ur (U.Vector Int) %1 -> Ur (U.Vector Int, Int)
attachCapacity =
Unsafe.toLinear2 \finalCapacity (Ur frozen) ->
Ur (frozen, finalCapacity)
pureBorrowGrowthWorker ::
forall α.
U.Vector Int ->
Int ->
Mut α (Growable.GrowableVector Int) %1 ->
BO α (Mut α (Growable.GrowableVector Int))
{-# NOINLINE pureBorrowGrowthWorker #-}
pureBorrowGrowthWorker input !index vector
| index >= U.length input = Control.pure vector
| otherwise = Control.do
vector <- Growable.push (U.unsafeIndex input index) vector
pureBorrowGrowthWorker input (index + 1) vector
growthTarget :: Int -> Int -> Int
{-# INLINE growthTarget #-}
growthTarget oldCapacity required
| required <= oldCapacity = oldCapacity
| oldCapacity <= 0 = required `max` 1
| oldCapacity > maxBound `quot` 2 = required
| otherwise = required `max` (oldCapacity * 2)
pureBorrowWorker ::
forall α.
Int ->
Int ->
Mut α (Vector.Vector Int) %1 ->
BO α ()
{-# NOINLINE pureBorrowWorker #-}
pureBorrowWorker !length_ !index vector
| index >= length_ = Control.pure (consume vector)
| otherwise = Control.do
((), vector) <-
Vector.unsafeUpdate
index
(\ !value -> Control.pure ((), value + 1))
vector
pureBorrowWorker length_ (index + 1) vector
owningBoxedQsortKernel :: B.Vector Int -> B.Vector Int
{-# NOINLINE owningBoxedQsortKernel #-}
owningBoxedQsortKernel input =
unur $ linearly \linear -> DataFlow.do
(ownerLinear, runLinear) <- dup linear
runBO runLinear Control.do
(vector, lend) <-
borrowM (OwningVector.fromVector input ownerLinear)
OwningVector.qsort 0 vector
pureAfter (OwningVector.toVector (reclaim lend))
unrestrictedQsortKernel ::
(G.Vector v Int) =>
v Int ->
v Int
{-# INLINEABLE unrestrictedQsortKernel #-}
unrestrictedQsortKernel input =
unur $ linearly \linear -> DataFlow.do
(ownerLinear, runLinear) <- dup linear
runBO runLinear Control.do
(vector, lend) <-
borrowM (UnrestrictedVector.fromVector input ownerLinear)
DC.qsort 0 vector
pureAfter (UnrestrictedVector.toVector (reclaim lend))
unrestrictedBoxedQsortKernel :: B.Vector Int -> B.Vector Int
{-# NOINLINE unrestrictedBoxedQsortKernel #-}
unrestrictedBoxedQsortKernel = unrestrictedQsortKernel
unrestrictedUnboxedQsortKernel :: U.Vector Int -> U.Vector Int
{-# NOINLINE unrestrictedUnboxedQsortKernel #-}
unrestrictedUnboxedQsortKernel = unrestrictedQsortKernel
unrestrictedFftKernel ::
(G.Vector v (Complex Double)) =>
v (Complex Double) ->
v (Complex Double)
{-# INLINEABLE unrestrictedFftKernel #-}
unrestrictedFftKernel input =
unur $ linearly \linear -> DataFlow.do
(ownerLinear, runLinear) <- dup linear
runBO runLinear Control.do
(vector, lend) <-
borrowM (UnrestrictedVector.fromVector input ownerLinear)
Control.void $
DC.sequentialDivideAndConquer
(DC.fftDC' 128)
vector
pureAfter (UnrestrictedVector.toVector (reclaim lend))
unrestrictedBoxedFftKernel ::
B.Vector (Complex Double) ->
B.Vector (Complex Double)
{-# NOINLINE unrestrictedBoxedFftKernel #-}
unrestrictedBoxedFftKernel = unrestrictedFftKernel
unrestrictedUnboxedFftKernel ::
U.Vector (Complex Double) ->
U.Vector (Complex Double)
{-# NOINLINE unrestrictedUnboxedFftKernel #-}
unrestrictedUnboxedFftKernel = unrestrictedFftKernel
defaultMain :: IO ()
defaultMain = Bench.defaultMain benches
benches :: [Benchmark]
benches =
NonLinear.concat
[ [ env
(NonLinear.pure $ U.generate length_ (`NonLinear.rem` 1024))
\input ->
bgroup
("kernel/fixed-unboxed/" <> show length_)
[ bench "direct" $ nf directFixedUnboxedKernel input
, bench "pure-borrow" $ nf pureBorrowFixedUnboxedKernel input
]
, env
(NonLinear.pure $ U.generate length_ (`NonLinear.rem` 1024))
\input ->
bgroup
("kernel/growable-unboxed/no-growth/" <> show length_)
[ bench "direct" $ nf directGrowableUnboxedNoGrowthKernel input
, bench "pure-borrow" $ nf pureBorrowGrowableUnboxedNoGrowthKernel input
]
, env
(NonLinear.pure $ U.generate length_ (`NonLinear.rem` 1024))
\input ->
bgroup
("kernel/growable-unboxed/growth/" <> show length_)
[ bench "direct" $ nf directGrowableUnboxedGrowthKernel input
, bench "pure-borrow" $ nf pureBorrowGrowableUnboxedGrowthKernel input
]
, env
(NonLinear.pure $ U.generate length_ (`NonLinear.rem` 1024))
\input ->
bgroup
("public-materialization/fixed-unboxed/" <> show length_)
[ bench "direct" $ nf directFixedUnboxedMaterialization input
, bench "pure-borrow" $ nf pureBorrowFixedUnboxedMaterialization input
]
, env
(NonLinear.pure $ U.generate length_ (`NonLinear.rem` 1024))
\input ->
bgroup
("public-materialization/growable-unboxed/" <> show length_)
[ bench "direct" $ nf directGrowableUnboxedMaterialization input
, bench "pure-borrow" $ nf pureBorrowGrowableUnboxedMaterialization input
]
]
| length_ <- [0, 1, 1024, 1024 * 1024]
]
<> [ env
( NonLinear.pure
( B.generate length_ (comparisonQsortValue length_)
, U.generate length_ (comparisonQsortValue length_)
)
)
\ ~(boxedInput, unboxedInput) ->
bgroup
( "algorithm/qsort/storage-and-element-ownership/"
<> show length_
)
[ bench "owning/boxed" $
nf owningBoxedQsortKernel boxedInput
, bench "unrestricted/boxed" $
nf unrestrictedBoxedQsortKernel boxedInput
, bench "unrestricted/unboxed" $
nf unrestrictedUnboxedQsortKernel unboxedInput
]
| length_ <- [8 * 1024, 32 * 1024]
]
<> [ env
( NonLinear.pure
( B.generate length_ comparisonFftValue
, U.generate length_ comparisonFftValue
)
)
\ ~(boxedInput, unboxedInput) ->
bgroup
( "algorithm/fft/storage-and-element-ownership/"
<> show length_
)
[ bench "unrestricted/boxed" $
nf unrestrictedBoxedFftKernel boxedInput
, bench "unrestricted/unboxed" $
nf unrestrictedUnboxedFftKernel unboxedInput
]
| length_ <- [64 * 1024, 1024 * 1024]
]
comparisonQsortValue :: Int -> Int -> Int
comparisonQsortValue length_ index =
(index * 1103515245 + 12345) `NonLinear.rem` (length_ + 1)
comparisonFftValue :: Int -> Complex Double
comparisonFftValue index =
let position = fromIntegral index
in (sin (position / 17) + cos (position / 31)) :+ 0