Skip to content

Conversation

@effectfully
Copy link
Contributor

As per @kwxm's request.

Copy link
Contributor

@kwxm kwxm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! This all looks good. I plugged this into my benchmarking code, and using the new folds is about 20% faster for summing up lists than the versions using ifThenElse. That's better than I was expecting, and it'll give us a fairer comparison against the compiled-from-Haskell versions.

@kwxm kwxm merged commit d9f7c1d into IntersectMBO:master Oct 17, 2021
@effectfully effectfully deleted the effectfully/builtin/caseList-in-terms-of-chooseList branch October 17, 2021 13:23
@kwxm kwxm mentioned this pull request Oct 18, 2021
8 tasks
@effectfully effectfully restored the effectfully/builtin/caseList-in-terms-of-chooseList branch March 3, 2022 14:32
@effectfully
Copy link
Contributor Author

/benchmark plutus-benchmark:validation

@effectfully effectfully deleted the effectfully/builtin/caseList-in-terms-of-chooseList branch March 3, 2022 16:13
@effectfully effectfully restored the effectfully/builtin/caseList-in-terms-of-chooseList branch March 3, 2022 16:13
@effectfully effectfully deleted the effectfully/builtin/caseList-in-terms-of-chooseList branch March 3, 2022 16:14
effectfully added a commit that referenced this pull request Oct 18, 2024
The main change is replacing

```haskell
data BuiltinRuntime val
    = BuiltinCostedResult ExBudgetStream ~(BuiltinResult val)
    | <...>
```

with

```haskell
data BuiltinRuntime val
    = BuiltinCostedResult ExBudgetStream ~(BuiltinResult (HeadSpine val))
    | <...>
```

where `HeadSpine` is a fancy way of saying `NonEmpty`:

```haskell
-- | A non-empty spine. Isomorphic to 'NonEmpty', except is strict and is defined as a single
-- recursive data type.
data Spine a
    = SpineLast a
    | SpineCons a (Spine a)

-- | The head-spine form of an iterated application. Provides O(1) access to the head of the
-- application. Isomorphic to @nonempty@, except is strict and the no-spine case is made a separate
-- constructor for performance reasons (it only takes a single pattern match to access the head when
-- there's no spine this way, while otherwise we'd also need to match on the spine to ensure that
-- it's empty -- and the no-spine case is by far the most common one, hence we want to optimize it).
data HeadSpine a
    = HeadOnly a
    | HeadSpine a (Spine a)
```

(we define a separate type, because we want strictness, and you don't see any bangs, because it's in a module with `StrictData` enabled).

The idea is that a builtin application can return a function applied to a bunch of arguments, which is exactly what we need to be able to express `caseList`

```haskell
caseList xs0 f z = case xs0 of
   []   -> z
   x:xs -> f x xs
```

as a builtin:

```haskell
-- | Take a function and a list of arguments and apply the former to the latter.
headSpine :: Opaque val asToB -> [val] -> Opaque (HeadSpine val) b
headSpine (Opaque f) = Opaque . \case
    []      -> HeadOnly f
    x0 : xs ->
        -- It's critical to use 'foldr' here, so that deforestation kicks in.
        -- See Note [Definition of foldl'] in "GHC.List" and related Notes around for an explanation
        -- of the trick.
        HeadSpine f $ foldr (\x2 r x1 -> SpineCons x1 $ r x2) SpineLast xs x0

instance uni ~ DefaultUni => ToBuiltinMeaning uni DefaultFun where
    <...>
    toBuiltinMeaning _ver CaseList =
        let caseListDenotation
                :: Opaque val (LastArg a b)
                -> Opaque val (a -> [a] -> b)
                -> SomeConstant uni [a]
                -> BuiltinResult (Opaque (HeadSpine val) b)
            caseListDenotation z f (SomeConstant (Some (ValueOf uniListA xs0))) = do
                case uniListA of
                    DefaultUniList uniA -> pure $ case xs0 of
                        []     -> headSpine z []                                             -- [1]
                        x : xs -> headSpine f [fromValueOf uniA x, fromValueOf uniListA xs]  -- [2]
                    _ ->
                        -- See Note [Structural vs operational errors within builtins].
                        throwing _StructuralUnliftingError "Expected a list but got something else"
            {-# INLINE caseListDenotation #-}
        in makeBuiltinMeaning
            caseListDenotation
            (runCostingFunThreeArguments . unimplementedCostingFun)
```

Being able to express [1] (representing `z`) and [2] (representing `f x xs`) is precisely what this PR enables.

Adding support for the new functionality to the CEK machine is trivial. All we need is a way to push a `Spine` of arguments onto the context:

```haskell
    -- | Push arguments onto the stack. The first argument will be the most recent entry.
    pushArgs
        :: Spine (CekValue uni fun ann)
        -> Context uni fun ann
        -> Context uni fun ann
    pushArgs args ctx = foldr FrameAwaitFunValue ctx args
```

and a `HeadSpine` version of `returnCek`:

```haskell
    -- | Evaluate a 'HeadSpine' by pushing the arguments (if any) onto the stack and proceeding with
    -- the returning phase of the CEK machine.
    returnCekHeadSpine
        :: Context uni fun ann
        -> HeadSpine (CekValue uni fun ann)
        -> CekM uni fun s (Term NamedDeBruijn uni fun ())
    returnCekHeadSpine ctx (HeadOnly  x)    = returnCek ctx x
    returnCekHeadSpine ctx (HeadSpine f xs) = returnCek (pushArgs xs ctx) f
```

Then replacing

```haskell
                BuiltinSuccess x ->
                    returnCek ctx x
```

with

```haskell
                BuiltinSuccess fXs ->
                    returnCekHeadSpine ctx fXs
```

(and similarly for `BuiltinSuccessWithLogs`) will do the trick.

We used to define `caseList` in terms of `IfThenElse`, `NullList` and either `HeadList` or `TailList` depending on the result of `NullList`, i.e. three builtin calls in the worst and in the best case. Then we introduced `ChooseList`, which replaced both `IfThenElse` and `NullList` in `caseList` thus bringing total amount of builtin calls down to 2 in all cases. This turned out to have a [substantial](#4119 (review)) impact on performance. This PR allows us to bring total number of builtin calls per `caseList` invokation down to 1 -- the `CaseList` builtin itself.
v0d1ch pushed a commit to v0d1ch/plutus that referenced this pull request Dec 6, 2024
The main change is replacing

```haskell
data BuiltinRuntime val
    = BuiltinCostedResult ExBudgetStream ~(BuiltinResult val)
    | <...>
```

with

```haskell
data BuiltinRuntime val
    = BuiltinCostedResult ExBudgetStream ~(BuiltinResult (HeadSpine val))
    | <...>
```

where `HeadSpine` is a fancy way of saying `NonEmpty`:

```haskell
-- | A non-empty spine. Isomorphic to 'NonEmpty', except is strict and is defined as a single
-- recursive data type.
data Spine a
    = SpineLast a
    | SpineCons a (Spine a)

-- | The head-spine form of an iterated application. Provides O(1) access to the head of the
-- application. Isomorphic to @nonempty@, except is strict and the no-spine case is made a separate
-- constructor for performance reasons (it only takes a single pattern match to access the head when
-- there's no spine this way, while otherwise we'd also need to match on the spine to ensure that
-- it's empty -- and the no-spine case is by far the most common one, hence we want to optimize it).
data HeadSpine a
    = HeadOnly a
    | HeadSpine a (Spine a)
```

(we define a separate type, because we want strictness, and you don't see any bangs, because it's in a module with `StrictData` enabled).

The idea is that a builtin application can return a function applied to a bunch of arguments, which is exactly what we need to be able to express `caseList`

```haskell
caseList xs0 f z = case xs0 of
   []   -> z
   x:xs -> f x xs
```

as a builtin:

```haskell
-- | Take a function and a list of arguments and apply the former to the latter.
headSpine :: Opaque val asToB -> [val] -> Opaque (HeadSpine val) b
headSpine (Opaque f) = Opaque . \case
    []      -> HeadOnly f
    x0 : xs ->
        -- It's critical to use 'foldr' here, so that deforestation kicks in.
        -- See Note [Definition of foldl'] in "GHC.List" and related Notes around for an explanation
        -- of the trick.
        HeadSpine f $ foldr (\x2 r x1 -> SpineCons x1 $ r x2) SpineLast xs x0

instance uni ~ DefaultUni => ToBuiltinMeaning uni DefaultFun where
    <...>
    toBuiltinMeaning _ver CaseList =
        let caseListDenotation
                :: Opaque val (LastArg a b)
                -> Opaque val (a -> [a] -> b)
                -> SomeConstant uni [a]
                -> BuiltinResult (Opaque (HeadSpine val) b)
            caseListDenotation z f (SomeConstant (Some (ValueOf uniListA xs0))) = do
                case uniListA of
                    DefaultUniList uniA -> pure $ case xs0 of
                        []     -> headSpine z []                                             -- [1]
                        x : xs -> headSpine f [fromValueOf uniA x, fromValueOf uniListA xs]  -- [2]
                    _ ->
                        -- See Note [Structural vs operational errors within builtins].
                        throwing _StructuralUnliftingError "Expected a list but got something else"
            {-# INLINE caseListDenotation #-}
        in makeBuiltinMeaning
            caseListDenotation
            (runCostingFunThreeArguments . unimplementedCostingFun)
```

Being able to express [1] (representing `z`) and [2] (representing `f x xs`) is precisely what this PR enables.

Adding support for the new functionality to the CEK machine is trivial. All we need is a way to push a `Spine` of arguments onto the context:

```haskell
    -- | Push arguments onto the stack. The first argument will be the most recent entry.
    pushArgs
        :: Spine (CekValue uni fun ann)
        -> Context uni fun ann
        -> Context uni fun ann
    pushArgs args ctx = foldr FrameAwaitFunValue ctx args
```

and a `HeadSpine` version of `returnCek`:

```haskell
    -- | Evaluate a 'HeadSpine' by pushing the arguments (if any) onto the stack and proceeding with
    -- the returning phase of the CEK machine.
    returnCekHeadSpine
        :: Context uni fun ann
        -> HeadSpine (CekValue uni fun ann)
        -> CekM uni fun s (Term NamedDeBruijn uni fun ())
    returnCekHeadSpine ctx (HeadOnly  x)    = returnCek ctx x
    returnCekHeadSpine ctx (HeadSpine f xs) = returnCek (pushArgs xs ctx) f
```

Then replacing

```haskell
                BuiltinSuccess x ->
                    returnCek ctx x
```

with

```haskell
                BuiltinSuccess fXs ->
                    returnCekHeadSpine ctx fXs
```

(and similarly for `BuiltinSuccessWithLogs`) will do the trick.

We used to define `caseList` in terms of `IfThenElse`, `NullList` and either `HeadList` or `TailList` depending on the result of `NullList`, i.e. three builtin calls in the worst and in the best case. Then we introduced `ChooseList`, which replaced both `IfThenElse` and `NullList` in `caseList` thus bringing total amount of builtin calls down to 2 in all cases. This turned out to have a [substantial](IntersectMBO#4119 (review)) impact on performance. This PR allows us to bring total number of builtin calls per `caseList` invokation down to 1 -- the `CaseList` builtin itself.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants