Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Builtins] Optimize costing by being lazier #5239

Merged
merged 26 commits into from
Apr 16, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a1c620e
Rebase on 'master'
effectfully Apr 11, 2023
a258b89
Custom equality and mappending
effectfully Mar 28, 2023
318ea75
Use Ziyang's 'flattenCostRose'
effectfully Mar 29, 2023
0912f26
'CostRose' as a sum type
effectfully Mar 29, 2023
f6f8f40
Revert "'CostRose' as a sum type"
effectfully Mar 29, 2023
389f0bd
Drop a half of the 'satint' library
effectfully Mar 6, 2023
a3be16c
Separate stream types and 'CostRose' into their own modules
effectfully Mar 31, 2023
ec9ef1e
Fix some of the build
effectfully Mar 31, 2023
dc60fdb
Ada a changelog entry
effectfully Apr 3, 2023
bfc2794
Address a couple of comments
effectfully Apr 3, 2023
902ae19
'toSatInt' -> 'unsafeToSatInt'
effectfully Apr 3, 2023
bba3544
'fromIntegral . unSatInt' -> 'fromSatInt'
effectfully Apr 3, 2023
1a98a20
Add Note [Single-element streams]
effectfully Apr 3, 2023
8ce30ad
Document the 'ExMemoryUsage' module
effectfully Apr 3, 2023
c1535c8
Drop a redundant 'CostRose 0'
effectfully Apr 3, 2023
5e5dc10
Document 'TrackCosts'
effectfully Apr 4, 2023
58f50f9
Improve 'TrackCosts'
effectfully Apr 4, 2023
847f73b
Document 'TrackCosts' tests
effectfully Apr 4, 2023
7b46fdd
Add a comment
effectfully Apr 4, 2023
b28366c
Improve tests and document a part of them
effectfully Apr 7, 2023
9ef7285
Document more in the 'Costing.hs' module
effectfully Apr 11, 2023
b73f740
Address more comments
effectfully Apr 11, 2023
a24af51
Add 'test_costsAreNeverNegative'
effectfully Apr 12, 2023
6843cac
Finish off docs
effectfully Apr 13, 2023
8d9da85
Fix whatever stupid random mistake it was
effectfully Apr 13, 2023
19c82da
Final tweaks
effectfully Apr 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
'CostRose' as a sum type
  • Loading branch information
effectfully committed Apr 13, 2023
commit 0912f261fd3f92d640f3cb550509c7b7c4a5c6ac
2 changes: 1 addition & 1 deletion plutus-core/plutus-core/src/PlutusCore/Default/Builtins.hs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ instance Pretty DefaultFun where
c : s -> toLower c : s

instance ExMemoryUsage DefaultFun where
memoryUsage _ = CostRose 1 []
memoryUsage _ = toCostRose 1 []

-- | Turn a function into another function that returns 'EvaluationFailure' when its second argument
-- is 0 or calls the original function otherwise and wraps the result in 'EvaluationSuccess'.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ module PlutusCore.Evaluation.Machine.ExMemory
, mapCostStream
, addCostStream
, minCostStream
, toCostRose
, flattenCostRose
) where

Expand Down Expand Up @@ -205,20 +206,36 @@ traversing the list), while we of course want it to be O(1).
-- 'CostRose' gets collapsed to a lazy linear structure down the pipeline, so that we can
-- stream the costs to the outside where, say, the CEK machine picks them up one by one and handles
-- somehow (in particular, subtracts from the remaining budget).
data CostRose = CostRose {-# UNPACK #-} !CostingInteger ![CostRose]
data CostRose
= CostLeaf {-# UNPACK #-} !CostingInteger
| CostFork {-# UNPACK #-} !CostingInteger CostForest
deriving stock (Show)

data CostForest
= CostForestLast !CostRose
| CostForestCons !CostRose CostForest
deriving stock (Show)

toCostRose :: CostingInteger -> [CostRose] -> CostRose
toCostRose i [] = CostLeaf i
-- TODO: try filtering out zeros somehow?
toCostRose i (rose : roses) = CostFork i $ foldr step CostForestLast roses rose where
step :: CostRose -> (CostRose -> CostForest) -> CostRose -> CostForest
step new k old = CostForestCons old $ k new
{-# INLINE step #-}
{-# INLINE toCostRose #-}

class ExMemoryUsage a where
-- Inlining the implementations of this method gave us a 1-2% speedup.
memoryUsage :: a -> CostRose -- ^ How much memory does 'a' use?

instance (ExMemoryUsage a, ExMemoryUsage b) => ExMemoryUsage (a, b) where
memoryUsage (a, b) = CostRose 1 [memoryUsage a, memoryUsage b]
memoryUsage (a, b) = toCostRose 1 [memoryUsage a, memoryUsage b]
{-# INLINE memoryUsage #-}

-- See https://github.com/input-output-hk/plutus/issues/1861
instance ExMemoryUsage (SomeTypeIn uni) where
memoryUsage _ = CostRose 1 [] -- TODO things like @list (list (list integer))@ take up a non-constant amount of space.
memoryUsage _ = toCostRose 1 [] -- TODO things like @list (list (list integer))@ take up a non-constant amount of space.
{-# INLINE memoryUsage #-}

-- See https://github.com/input-output-hk/plutus/issues/1861
Expand All @@ -228,7 +245,7 @@ instance (Closed uni, uni `Everywhere` ExMemoryUsage) => ExMemoryUsage (Some (Va
{-# INLINE memoryUsage #-}

instance ExMemoryUsage () where
memoryUsage () = CostRose 1 []
memoryUsage () = toCostRose 1 []
{-# INLINE memoryUsage #-}

memoryUsageInteger :: Integer -> CostingInteger
Expand All @@ -241,11 +258,11 @@ memoryUsageInteger i = fromIntegral $ I# (integerLog2# (abs i) `quotInt#` intege
{-# NOINLINE memoryUsageInteger #-}

instance ExMemoryUsage Integer where
memoryUsage i = CostRose (memoryUsageInteger i) [] where
memoryUsage i = toCostRose (memoryUsageInteger i) [] where
{-# INLINE memoryUsage #-}

instance ExMemoryUsage Word8 where
memoryUsage _ = CostRose 1 []
memoryUsage _ = toCostRose 1 []
{-# INLINE memoryUsage #-}

{- Bytestrings: we want things of length 0 to have size 0, 1-8 to have size 1,
Expand All @@ -254,7 +271,7 @@ instance ExMemoryUsage Word8 where
1 + (toInteger $ BS.length bs) `div` 8, which would count one extra for
things whose sizes are multiples of 8. -}
instance ExMemoryUsage BS.ByteString where
memoryUsage bs = CostRose (((n-1) `quot` 8) + 1) [] -- Don't use `div` here! That gives 1 instead of 0 for n=0.
memoryUsage bs = toCostRose (((n-1) `quot` 8) + 1) [] -- Don't use `div` here! That gives 1 instead of 0 for n=0.
where n = fromIntegral $ BS.length bs :: SatInt
{-# INLINE memoryUsage #-}

Expand All @@ -265,19 +282,19 @@ instance ExMemoryUsage T.Text where
{-# INLINE memoryUsage #-}

instance ExMemoryUsage Int where
memoryUsage _ = CostRose 1 []
memoryUsage _ = toCostRose 1 []
{-# INLINE memoryUsage #-}

instance ExMemoryUsage Char where
memoryUsage _ = CostRose 1 []
memoryUsage _ = toCostRose 1 []
{-# INLINE memoryUsage #-}

instance ExMemoryUsage Bool where
memoryUsage _ = CostRose 1 []
memoryUsage _ = toCostRose 1 []
{-# INLINE memoryUsage #-}

instance ExMemoryUsage a => ExMemoryUsage [a] where
memoryUsage = CostRose 0 . map memoryUsage
memoryUsage = toCostRose 0 . map memoryUsage
{-# INLINE memoryUsage #-}

{- Another naive traversal for size. This accounts for the number of nodes in
Expand All @@ -303,18 +320,14 @@ instance ExMemoryUsage a => ExMemoryUsage [a] where
-}
instance ExMemoryUsage Data where
memoryUsage = sizeData where
nodeMem = CostRose 4 []
{-# INLINE nodeMem #-}
addNodeMem = toCostRose 4 . pure
{-# INLINE addNodeMem #-}

combine (CostRose cost1 forest1) (CostRose cost2 forest2) =
CostRose (cost1 + cost2) (forest1 ++ forest2)
{-# INLINE combine #-}

sizeData d = combine nodeMem $ case d of
sizeData d = addNodeMem $ case d of
-- TODO: include the size of the tag, but not just yet. See SCP-3677.
Constr _ l -> CostRose 0 $ l <&> sizeData
Map l -> CostRose 0 $ l <&> \(d1, d2) -> CostRose 0 $ [d1, d2] <&> sizeData
List l -> CostRose 0 $ l <&> sizeData
Constr _ l -> toCostRose 0 $ l <&> sizeData
Map l -> toCostRose 0 $ l <&> \(d1, d2) -> toCostRose 0 $ [d1, d2] <&> sizeData
List l -> toCostRose 0 $ l <&> sizeData
I n -> memoryUsage n
B b -> memoryUsage b

Expand Down Expand Up @@ -415,19 +428,33 @@ minCostStream costsL0 costsR0 = case (costsL0, costsR0) of
_ -> minCostStreamGo costsL0 costsR0
{-# INLINE minCostStream #-}

appendCostForest :: CostForest -> CostForest -> CostForest
appendCostForest (CostForestLast rose1) forest2 = CostForestCons rose1 forest2
appendCostForest (CostForestCons rose1 forest1) forest2 =
CostForestCons rose1 $ appendCostForest forest1 forest2

-- See Note [Global local functions].
flattenCostRoseForestGo :: CostRose -> CostForest -> CostStream
flattenCostRoseForestGo (CostLeaf cost1) forest2 =
CostCons cost1 $ flattenCostForestGo forest2
flattenCostRoseForestGo (CostFork cost1 forest1) forest2 =
CostCons cost1 $ case forest1 of
CostForestLast rose1' -> flattenCostRoseForestGo rose1' forest2
CostForestCons rose1' forest1' ->
flattenCostRoseForestGo rose1' $ appendCostForest forest1' forest2

-- See Note [Global local functions].
-- Exact copy of 'flattenCostRose'.
flattenCostRoseGo :: CostRose -> CostStream
flattenCostRoseGo (CostLeaf cost) = CostLast cost
flattenCostRoseGo (CostFork cost forest) = CostCons cost $ flattenCostForestGo forest

-- See Note [Global local functions].
flattenCostRoseGo :: CostRose -> [CostRose] -> CostStream
flattenCostRoseGo (CostRose cost1 forest1) forest2 =
case forest1 of
[] -> case forest2 of
[] -> CostLast cost1
rose2' : forest2' -> CostCons cost1 $ flattenCostRoseGo rose2' forest2'
rose1' : forest1' ->
CostCons cost1 $ case forest1' of
[] -> flattenCostRoseGo rose1' forest2
_ -> flattenCostRoseGo rose1' $ forest1' ++ forest2
flattenCostForestGo :: CostForest -> CostStream
flattenCostForestGo (CostForestLast rose) = flattenCostRoseGo rose
flattenCostForestGo (CostForestCons rose forest) = flattenCostRoseForestGo rose forest

flattenCostRose :: CostRose -> CostStream
flattenCostRose (CostRose cost []) = CostLast cost
flattenCostRose (CostRose cost (rose : forest)) = CostCons cost $ flattenCostRoseGo rose forest
flattenCostRose (CostLeaf cost) = CostLast cost
flattenCostRose (CostFork cost forest) = CostCons cost $ flattenCostForestGo forest
{-# INLINE flattenCostRose #-}
Original file line number Diff line number Diff line change
Expand Up @@ -570,10 +570,10 @@ data Context uni fun ann
instance (Closed uni, uni `Everywhere` ExMemoryUsage) => ExMemoryUsage (CekValue uni fun ann) where
memoryUsage = \case
VCon c -> memoryUsage c
VDelay {} -> CostRose 1 []
VLamAbs {} -> CostRose 1 []
VBuiltin {} -> CostRose 1 []
VConstr {} -> CostRose 1 []
VDelay {} -> toCostRose 1 []
VLamAbs {} -> toCostRose 1 []
VBuiltin {} -> toCostRose 1 []
VConstr {} -> toCostRose 1 []
{-# INLINE memoryUsage #-}

-- | A 'MonadError' version of 'try'.
Expand Down
78 changes: 39 additions & 39 deletions plutus-core/untyped-plutus-core/test/Evaluation/Builtins/Costing.hs
effectfully marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,8 @@ cndSize n0
-- The parameter is the depth of the tree.
cndRose :: Int -> CostRose
cndRose n0
| n0 <= 1 = CostRose 1 []
| otherwise = CostRose (fromIntegral n0) . go . replicate 3 $ n0 - 1
| n0 <= 1 = toCostRose 1 []
| otherwise = toCostRose (fromIntegral n0) . go . replicate 3 $ n0 - 1
where
-- Inlining the definition of @map cndRose@ manually to make sure subtrees are definitely
-- not shared, so that we don't retain them in memory unnecessarily.
Expand Down Expand Up @@ -333,20 +333,20 @@ multiSplit xs = do

genCostRose :: NonEmptyList SatInt -> Gen CostRose
genCostRose (NonEmpty []) = error "an impossible happened"
genCostRose (NonEmpty (cost : costs)) = CostRose cost <$> do
genCostRose (NonEmpty (cost : costs)) = toCostRose cost <$> do
forest <- multiSplit costs
traverse genCostRose forest

fromCostRose :: CostRose -> NonEmptyList SatInt
fromCostRose (CostRose cost costs) =
NonEmpty $ cost : concatMap (getNonEmpty . fromCostRose) costs
-- fromCostRose :: CostRose -> NonEmptyList SatInt
-- fromCostRose (CostRose cost costs) =
-- NonEmpty $ cost : concatMap (getNonEmpty . fromCostRose) costs

instance Arbitrary CostRose where
arbitrary = arbitrary >>= genCostRose

shrink (CostRose cost costs) = do
(costs', cost') <- shrink (costs, cost)
pure $ CostRose cost' costs'
-- shrink (CostRose cost costs) = do
-- (costs', cost') <- shrink (costs, cost)
-- pure $ toCostRose cost' costs'

test_multiSplitDistributionAt :: Int -> TestTree
effectfully marked this conversation as resolved.
Show resolved Hide resolved
test_multiSplitDistributionAt n =
Expand All @@ -364,33 +364,33 @@ test_multiSplitDistribution =
, test_multiSplitDistributionAt 5
]

collectListLengths :: CostRose -> [Int]
collectListLengths (CostRose _ costs) = length costs : concatMap collectListLengths costs

test_CostRoseDistribution :: TestTree
test_CostRoseDistribution =
testProperty "distribution of list lengths in CostRose values" $
withMaxSuccess 5000 $ \rose ->
tabulate "" (map show $ collectListLengths rose) True

test_genCostRoseSound :: TestTree
test_genCostRoseSound =
testProperty "genCostRose puts 100% of its input and nothing else into the output" $
withMaxSuccess 5000 $ \costs ->
forAll (genCostRose costs) $ \rose ->
fromCostRose rose ===
costs

test_flattenCostRoseSound :: TestTree
test_flattenCostRoseSound =
testProperty "flattenCostRose puts 100% of its input and nothing else into the output" $
withMaxSuccess 5000 $ \rose ->
-- This assumes that 'flattenCostRose' is left-biased, which isn't really
-- necessarily, but it doesn't seem like we're giving up on the assumption any time soon
-- anyway, so why not keep it simple instead of sorting the results.
checkEqualsVia eqCostStream
(flattenCostRose rose)
(fromCostList $ fromCostRose rose)
-- collectListLengths :: CostRose -> [Int]
-- collectListLengths (CostRose _ costs) = length costs : concatMap collectListLengths costs

-- test_CostRoseDistribution :: TestTree
-- test_CostRoseDistribution =
-- testProperty "distribution of list lengths in CostRose values" $
-- withMaxSuccess 5000 $ \rose ->
-- tabulate "" (map show $ collectListLengths rose) True

-- test_genCostRoseSound :: TestTree
-- test_genCostRoseSound =
-- testProperty "genCostRose puts 100% of its input and nothing else into the output" $
-- withMaxSuccess 5000 $ \costs ->
-- forAll (genCostRose costs) $ \rose ->
-- fromCostRose rose ===
-- costs

-- test_flattenCostRoseSound :: TestTree
-- test_flattenCostRoseSound =
-- testProperty "flattenCostRose puts 100% of its input and nothing else into the output" $
-- withMaxSuccess 5000 $ \rose ->
-- -- This assumes that 'flattenCostRose' is left-biased, which isn't really
-- -- necessarily, but it doesn't seem like we're giving up on the assumption any time soon
-- -- anyway, so why not keep it simple instead of sorting the results.
-- checkEqualsVia eqCostStream
-- (flattenCostRose rose)
-- (fromCostList $ fromCostRose rose)

test_costing :: TestTree
test_costing = testGroup "costing"
Expand All @@ -414,7 +414,7 @@ test_costing = testGroup "costing"
, test_zipCostStreamHandlesBottom
, test_flattenCostRoseIsLinear
, test_multiSplitDistribution
, test_CostRoseDistribution
, test_genCostRoseSound
, test_flattenCostRoseSound
-- , test_CostRoseDistribution
-- , test_genCostRoseSound
-- , test_flattenCostRoseSound
]