Skip to content

Commit b8363fc

Browse files
authored
[Builtins] Add an inlinable version of 'geq' (#7323)
Finally discovered a way to create an inlinable version of `geq`.
1 parent b12c894 commit b8363fc

File tree

4 files changed

+173
-76
lines changed

4 files changed

+173
-76
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### Changed
2+
3+
- In #7323 made the `Constr`, `List` and `Map` builtins 7+% faster.

plutus-core/plutus-core/src/PlutusCore/Builtin/KnownType.hs

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
1-
{-# LANGUAGE BlockArguments #-}
2-
{-# LANGUAGE ConstraintKinds #-}
3-
{-# LANGUAGE DataKinds #-}
4-
{-# LANGUAGE DefaultSignatures #-}
5-
{-# LANGUAGE FlexibleInstances #-}
6-
{-# LANGUAGE FunctionalDependencies #-}
7-
{-# LANGUAGE LambdaCase #-}
8-
{-# LANGUAGE MultiParamTypeClasses #-}
9-
{-# LANGUAGE OverloadedStrings #-}
10-
{-# LANGUAGE TemplateHaskell #-}
11-
{-# LANGUAGE TypeApplications #-}
12-
{-# LANGUAGE TypeFamilies #-}
13-
{-# LANGUAGE TypeOperators #-}
14-
{-# LANGUAGE UndecidableInstances #-}
15-
16-
{-# LANGUAGE StrictData #-}
1+
{-# LANGUAGE BlockArguments #-}
2+
{-# LANGUAGE ConstraintKinds #-}
3+
{-# LANGUAGE DataKinds #-}
4+
{-# LANGUAGE DefaultSignatures #-}
5+
{-# LANGUAGE FlexibleInstances #-}
6+
{-# LANGUAGE FunctionalDependencies #-}
7+
{-# LANGUAGE LambdaCase #-}
8+
{-# LANGUAGE MultiParamTypeClasses #-}
9+
{-# LANGUAGE OverloadedStrings #-}
10+
{-# LANGUAGE StandaloneKindSignatures #-}
11+
{-# LANGUAGE TemplateHaskell #-}
12+
{-# LANGUAGE TypeApplications #-}
13+
{-# LANGUAGE TypeFamilies #-}
14+
{-# LANGUAGE TypeOperators #-}
15+
{-# LANGUAGE UndecidableInstances #-}
16+
17+
{-# LANGUAGE StrictData #-}
1718

1819
module PlutusCore.Builtin.KnownType
1920
( BuiltinError
21+
, GEqL (..)
22+
, LoopBreaker (..)
2023
, KnownBuiltinTypeIn
2124
, KnownBuiltinType
2225
, BuiltinResult (..)
@@ -48,17 +51,52 @@ import Control.Monad.Except
4851
import Data.Bifunctor
4952
import Data.Either.Extras
5053
import Data.Functor.Identity
54+
import Data.Kind qualified as GHC
5155
import Data.String
5256
import GHC.Exts (inline, oneShot)
5357
import GHC.TypeLits
5458
import Prettyprinter
5559
import Text.PrettyBy.Internal
5660
import Universe
5761

62+
-- | A version of 'GEq' that fixes @a@ in place, which allows us to create an inlinable recursive
63+
-- implementation of 'geqL'.
64+
--
65+
-- The way it works is that whenever there's recursion, we look up the recursive case in the current
66+
-- context (i.e. the dictionary) instead of actually calling 'geqL' recursively (even though it's
67+
-- gonna look like we do exactly that, because there's no way to distinguish between a recursive
68+
-- call and a dictionary lookup as the two share the same name, although to help GHC choose a lookup
69+
-- we sprinkle the perhaps unreliable 'LoopBreaker' in the 'DefaultUni' instance of this class).
70+
--
71+
-- Alligning things this way allows us to inline arbitrarily deep recursion for as long as types
72+
-- keep being monomorphic.
73+
--
74+
-- For example, the 'MapData' builtin accepts a @[(Data, Data)]@ and with 'geqL' matching on all of
75+
-- 'DefaultUniProtoList', 'DefaultUniProtoPair' and 'DefaultUniData' gets inlined in the denotation
76+
-- of the builtin. For the 'Constr' builtin that resulted in a 4.3% speedup at the time this comment
77+
-- was written.
78+
type GEqL :: (GHC.Type -> GHC.Type) -> GHC.Type -> GHC.Constraint
79+
class GEqL f a where
80+
geqL :: f (Esc a) -> f (Esc b) -> EvaluationResult (a :~: b)
81+
82+
-- | In @f = ... f ...@ where @f@ is a class method, how do you know if @f@ is going to be a
83+
-- recursive call or a type class method call? If both type check, then you don't really know how
84+
-- GHC is going to play it. So we add this data type to make sure that the RHS @f@ will have to
85+
-- become a type class method call.
86+
--
87+
-- Can GHC turn that method call into a recursive one once type classes are resolved? Dunno, but at
88+
-- least we've introduced an obstacle preventing GHC from immediately creating a non-inlinable
89+
-- recursive definition.
90+
newtype LoopBreaker uni a = LoopBreaker (uni a)
91+
92+
instance GEqL uni a => GEqL (LoopBreaker uni) a where
93+
geqL = coerce $ geqL @uni
94+
{-# INLINE geqL #-}
95+
5896
-- | A constraint for \"@a@ is a 'ReadKnownIn' and 'MakeKnownIn' by means of being included
5997
-- in @uni@\".
6098
type KnownBuiltinTypeIn uni val a =
61-
(HasConstantIn uni val, PrettyParens (SomeTypeIn uni), GEq uni, uni `HasTermLevel` a)
99+
(HasConstantIn uni val, PrettyParens (SomeTypeIn uni), GEqL uni a, uni `HasTermLevel` a)
62100

63101
-- | A constraint for \"@a@ is a 'ReadKnownIn' and 'MakeKnownIn' by means of being included
64102
-- in @UniOf term@\".
@@ -277,9 +315,10 @@ readKnownConstant val = asConstant val >>= oneShot \case
277315
-- 'geq' matches on its first argument first, so we make the type tag that will be known
278316
-- statically (because this function will be inlined) go first in order for GHC to
279317
-- optimize some of the matching away.
280-
case uniExp `geq` uniAct of
281-
Just Refl -> pure x
282-
Nothing -> throwError $ BuiltinUnliftingEvaluationError $ typeMismatchError uniExp uniAct
318+
case uniExp `geqL` uniAct of
319+
EvaluationSuccess Refl -> pure x
320+
EvaluationFailure ->
321+
throwError . BuiltinUnliftingEvaluationError $ typeMismatchError uniExp uniAct
283322
{-# INLINE readKnownConstant #-}
284323

285324
-- | A non-empty spine. Isomorphic to 'NonEmpty', except is strict and is defined as a single

plutus-core/plutus-core/src/PlutusCore/Default/Builtins.hs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -844,12 +844,12 @@ Our final example is this:
844844
:: SomeConstant uni a -> SomeConstant uni [a] -> BuiltinResult (Opaque val [a])
845845
mkConsDenotation
846846
(SomeConstant (Some (ValueOf uniA x)))
847-
(SomeConstant (Some (ValueOf uniListA xs))) = do
847+
(SomeConstant (Some (ValueOf uniListA xs))) =
848848
case uniListA of
849849
DefaultUniList uniA' -> case uniA `geq` uniA' of -- [1]
850850
Just Refl -> -- [2]
851851
pure . fromValueOf uniListA $ x : xs -- [3]
852-
_ -> throwError $ structuralUnliftingError
852+
Nothing -> throwError $ structuralUnliftingError
853853
"The type of the value does not match the type of elements in the list"
854854
_ -> throwError $ structuralUnliftingError "Expected a list but got something else"
855855
{-# INLINE mkConsDenotation #-}
@@ -1425,7 +1425,7 @@ instance uni ~ DefaultUni => ToBuiltinMeaning uni DefaultFun where
14251425
case uniListA of
14261426
DefaultUniList uniA' -> case uniA `geq` uniA' of
14271427
Just Refl -> pure . fromValueOf uniListA $ x : xs
1428-
_ -> throwError $ structuralUnliftingError
1428+
Nothing -> throwError $ structuralUnliftingError
14291429
"The type of the value does not match the type of elements in the list"
14301430
_ -> throwError $ structuralUnliftingError "Expected a list but got something else"
14311431
{-# INLINE mkConsDenotation #-}

plutus-core/plutus-core/src/PlutusCore/Default/Universe.hs

Lines changed: 108 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,57 @@ pattern DefaultUniArray uniA =
131131
pattern DefaultUniPair uniA uniB =
132132
DefaultUniProtoPair `DefaultUniApply` uniA `DefaultUniApply` uniB
133133

134+
-- Removing 'LoopBreaker' didn't change anything at the time this comment was written, but we kept
135+
-- it, because it hopefully provides some additional assurance that 'geqL' will not get elaborated
136+
-- as a recursive definition.
137+
instance AllBuiltinArgs DefaultUni (GEqL DefaultUni) a => GEqL DefaultUni a where
138+
geqL DefaultUniInteger a2 = do
139+
DefaultUniInteger <- pure a2
140+
pure Refl
141+
geqL DefaultUniByteString a2 = do
142+
DefaultUniByteString <- pure a2
143+
pure Refl
144+
geqL DefaultUniString a2 = do
145+
DefaultUniString <- pure a2
146+
pure Refl
147+
geqL DefaultUniUnit a2 = do
148+
DefaultUniUnit <- pure a2
149+
pure Refl
150+
geqL DefaultUniBool a2 = do
151+
DefaultUniBool <- pure a2
152+
pure Refl
153+
geqL (DefaultUniProtoList `DefaultUniApply` a1) listA2 = do
154+
DefaultUniProtoList `DefaultUniApply` a2 <- pure listA2
155+
Refl <- geqL (LoopBreaker a1) (LoopBreaker a2)
156+
pure Refl
157+
geqL (DefaultUniProtoArray `DefaultUniApply` a1) arrayA2 = do
158+
DefaultUniProtoArray `DefaultUniApply` a2 <- pure arrayA2
159+
Refl <- geqL (LoopBreaker a1) (LoopBreaker a2)
160+
pure Refl
161+
geqL (DefaultUniProtoPair `DefaultUniApply` a1 `DefaultUniApply` b1) pairA2 = do
162+
DefaultUniProtoPair `DefaultUniApply` a2 `DefaultUniApply` b2 <- pure pairA2
163+
Refl <- geqL (LoopBreaker a1) (LoopBreaker a2)
164+
Refl <- geqL (LoopBreaker b1) (LoopBreaker b2)
165+
pure Refl
166+
geqL (f `DefaultUniApply` _ `DefaultUniApply` _ `DefaultUniApply` _) _ =
167+
noMoreTypeFunctions f
168+
geqL DefaultUniData a2 = do
169+
DefaultUniData <- pure a2
170+
pure Refl
171+
geqL DefaultUniBLS12_381_G1_Element a2 = do
172+
DefaultUniBLS12_381_G1_Element <- pure a2
173+
pure Refl
174+
geqL DefaultUniBLS12_381_G2_Element a2 = do
175+
DefaultUniBLS12_381_G2_Element <- pure a2
176+
pure Refl
177+
geqL DefaultUniBLS12_381_MlResult a2 = do
178+
DefaultUniBLS12_381_MlResult <- pure a2
179+
pure Refl
180+
geqL DefaultUniValue a2 = do
181+
DefaultUniValue <- pure a2
182+
pure Refl
183+
{-# INLINE geqL #-}
184+
134185
instance GEq DefaultUni where
135186
-- We define 'geq' manually instead of using 'deriveGEq', because the latter creates a single
136187
-- recursive definition and we want two instead. The reason why we want two is because this
@@ -140,59 +191,63 @@ instance GEq DefaultUni where
140191
-- (we're not really sure if this is a reliable solution, but if it stops working, we won't miss
141192
-- very much and we've failed to settle on any other approach).
142193
--
143-
-- This trick gives us a 1% speedup across validation benchmarks (some are up to 4% faster) and
144-
-- a more sensible generated Core where things like @geq DefaulUniBool@ are reduced away.
145-
geq = geqStep where
146-
geqStep :: DefaultUni a1 -> DefaultUni a2 -> Maybe (a1 :~: a2)
147-
geqStep DefaultUniInteger a2 = do
148-
DefaultUniInteger <- Just a2
149-
Just Refl
150-
geqStep DefaultUniByteString a2 = do
151-
DefaultUniByteString <- Just a2
152-
Just Refl
153-
geqStep DefaultUniString a2 = do
154-
DefaultUniString <- Just a2
155-
Just Refl
156-
geqStep DefaultUniUnit a2 = do
157-
DefaultUniUnit <- Just a2
158-
Just Refl
159-
geqStep DefaultUniBool a2 = do
160-
DefaultUniBool <- Just a2
161-
Just Refl
162-
geqStep DefaultUniProtoList a2 = do
163-
DefaultUniProtoList <- Just a2
164-
Just Refl
165-
geqStep DefaultUniProtoArray a2 = do
166-
DefaultUniProtoArray <- Just a2
167-
Just Refl
168-
geqStep DefaultUniProtoPair a2 = do
169-
DefaultUniProtoPair <- Just a2
170-
Just Refl
171-
geqStep (DefaultUniApply f1 x1) a2 = do
172-
DefaultUniApply f2 x2 <- Just a2
173-
Refl <- geqRec f1 f2
174-
Refl <- geqRec x1 x2
175-
Just Refl
176-
geqStep DefaultUniData a2 = do
177-
DefaultUniData <- Just a2
178-
Just Refl
179-
geqStep DefaultUniBLS12_381_G1_Element a2 = do
180-
DefaultUniBLS12_381_G1_Element <- Just a2
181-
Just Refl
182-
geqStep DefaultUniBLS12_381_G2_Element a2 = do
183-
DefaultUniBLS12_381_G2_Element <- Just a2
184-
Just Refl
185-
geqStep DefaultUniBLS12_381_MlResult a2 = do
186-
DefaultUniBLS12_381_MlResult <- Just a2
187-
Just Refl
188-
geqStep DefaultUniValue a2 = do
189-
DefaultUniValue <- Just a2
190-
Just Refl
191-
{-# INLINE geqStep #-}
192-
193-
geqRec :: DefaultUni a1 -> DefaultUni a2 -> Maybe (a1 :~: a2)
194-
geqRec = geqStep
195-
{-# OPAQUE geqRec #-}
194+
-- On the critical path this definition should only be used for builtins that perform equality
195+
-- checking of statically unknown runtime type tags ('MkCons' is one such builtin for
196+
-- example). All other builtins should use 'geqL' (the latter is internal to 'readKnownConstant'
197+
-- and is therefore hidden from the person adding a new builtin).
198+
--
199+
-- We use @NOINLINE@ instead of @OPAQUE@, because we don't actually care about the recursive
200+
-- definition not being inlined, we just want it to be chosen as the loop breaker.
201+
geq = goStep where
202+
goStep, goRec :: DefaultUni a1 -> DefaultUni a2 -> Maybe (a1 :~: a2)
203+
goStep DefaultUniInteger a2 = do
204+
DefaultUniInteger <- pure a2
205+
pure Refl
206+
goStep DefaultUniByteString a2 = do
207+
DefaultUniByteString <- pure a2
208+
pure Refl
209+
goStep DefaultUniString a2 = do
210+
DefaultUniString <- pure a2
211+
pure Refl
212+
goStep DefaultUniUnit a2 = do
213+
DefaultUniUnit <- pure a2
214+
pure Refl
215+
goStep DefaultUniBool a2 = do
216+
DefaultUniBool <- pure a2
217+
pure Refl
218+
goStep DefaultUniProtoList a2 = do
219+
DefaultUniProtoList <- pure a2
220+
pure Refl
221+
goStep DefaultUniProtoArray a2 = do
222+
DefaultUniProtoArray <- pure a2
223+
pure Refl
224+
goStep DefaultUniProtoPair a2 = do
225+
DefaultUniProtoPair <- pure a2
226+
pure Refl
227+
goStep (DefaultUniApply f1 x1) a2 = do
228+
DefaultUniApply f2 x2 <- pure a2
229+
Refl <- goRec f1 f2
230+
Refl <- goRec x1 x2
231+
pure Refl
232+
goStep DefaultUniData a2 = do
233+
DefaultUniData <- pure a2
234+
pure Refl
235+
goStep DefaultUniBLS12_381_G1_Element a2 = do
236+
DefaultUniBLS12_381_G1_Element <- pure a2
237+
pure Refl
238+
goStep DefaultUniBLS12_381_G2_Element a2 = do
239+
DefaultUniBLS12_381_G2_Element <- pure a2
240+
pure Refl
241+
goStep DefaultUniBLS12_381_MlResult a2 = do
242+
DefaultUniBLS12_381_MlResult <- pure a2
243+
pure Refl
244+
goStep DefaultUniValue a2 = do
245+
DefaultUniValue <- pure a2
246+
pure Refl
247+
{-# INLINE goStep #-}
248+
249+
goRec = goStep
250+
{-# NOINLINE goRec #-}
196251

197252
-- | For pleasing the coverage checker.
198253
noMoreTypeFunctions :: DefaultUni (Esc (f :: a -> b -> c -> d)) -> any

0 commit comments

Comments
 (0)