Other Optimization Techniques
Identifying problem areas
Profiling your script is a good way to identify which parts of the script are responsible for significant resource consumption. For more details, see Profiling the Budget Usage of Plutus Scripts.
Using a Recent Version of the Plutus Tx Compiler
The Plutus Tx compiler is available through the plutus-tx-plugin
package.
The Plutus team continuously improves compiler optimization, so using the latest or a recent version of plutus-tx-plugin
will likely result in more compact and efficient scripts.
Try conservative-optimisation
or Flags Implied by It
Certain optimizations, such as inlining constants, can occasionally have negative effects, making scripts larger or more expensive.
It is worth disabling them to see how it affects your script.
You can do this using the conservative-optimisation
plugin flag, which implies several other flags like no-inline-constants
.
Alternatively, try turning on the flags implied by conservative-optimisation
individually.
See Plutus Tx Compiler Options.
Using the Strict
Extension
The Strict
extension, which makes all bindings in a module strict, generally improves performance.
See GHC Extensions, Flags and Pragmas for an explanation.
However, care should be taken to avoid triggering unnecessary evaluations.
For example, in
let a = <expr1>
b = <expr2>
in a && b
b
will always be evaluated, even when a
evaluates to False
.
To avoid this, you can write either ~b = <expr2>
, or a && <expr2>
(recall that &&
and ||
are special in Plutus Tx in that their second arguments are non-strict, unlike ordinary Plutus Tx functions).
However, keep in mind that with ~b = <expr2>
, <expr2>
will be evaluated each time b
is referenced, since Plutus Tx does not employ lazy evaluation, i.e., there is no memoization.
Avoiding the INLINE
Pragma
The INLINE
pragma strongly encourages GHC to inline a function, even if it has a large body and is used multiple times.
This can lead to significant increase in the size of the resulting UPLC program, which is problematic since size is a much scarcer resource for Plutus scripts than for regular Haskell programs.
Instead, use the INLINEABLE
pragma.
This would leave most inlining decisions to the PIR and UPLC inliners, which are tailored for Plutus scripts and make more informed inlining decisions.
Be Mindful of Strict Applications
In Plutus Tx, as with all strict languages, function applications are strict (call by value), with the exception of a few special functions like &&
and ||
, which are treated specially by the compiler.
If you define your own version of &&
:
myAnd :: Bool -> Bool -> Bool
myAnd = (&&)
then it won't have the same behavior as &&
, as it will always evaluate both arguments, even if the first argument evaluates to False
.
It is particularly important to recognize that builtin functions like chooseList
and chooseData
are not special, i.e., they are also strict in all arguments.
Thus the following example, which directly invokes the chooseList
builtin, can be inefficient:
res = PlutusTx.Builtins.Internal.chooseList xs nilCase consCase
It may even be semantically incorrect, if nilCase = traceError "empty list"
, since it would always evaluate to an error.
Instead, use the wrapper provided by PlutusTx.Builtins
, which suspends the evaluation of nilCase
with a lambda:
res = PlutusTx.Builtins.matchList (\_ -> nilCase) consCase
Avoiding Intermediate Results
In a strict language, when composing several operations on a structure, the intermediate results are often fully materialized. As examples, consider
res1 = find (== 5) (xs ++ ys)
and
res2 = sum (Map.elems m)
These are perfectly efficient in Haskell, but since function applications are strict in Plutus Tx, the results of xs ++ ys
and Map.elems m
will be fully materialized before invoking find
and sum
, respectively.
You might consider rewriting these expressions to be less succinct but more efficient.
Specializing higher-order functions
The use of higher-order functions is a common technique to facilitate code reuse. Higher-order functions are widely used in the Plutus libraries but can be less efficient than specialized versions.
For instance, the Plutus function findOwnInput
makes use of the higher-order function find
to search for the current script input.
findOwnInput :: ScriptContext -> Maybe TxInInfo
findOwnInput ScriptContext{scriptContextTxInfo=TxInfo{txInfoInputs},
scriptContextPurpose=Spending txOutRef} =
find (\TxInInfo{txInInfoOutRef} -> txInInfoOutRef == txOutRef) txInfoInputs
findOwnInput _ = Nothing
This can be rewritten with a recursive function specialized to the specific check in question.
findOwnInput :: ScriptContext -> Maybe TxInInfo
findOwnInput ScriptContext{scriptContextTxInfo=TxInfo{txInfoInputs},
scriptContextPurpose=Spending txOutRef} = go txInfoInputs
where
go [] = Nothing
go (i@TxInInfo{txInInfoOutRef} : rest) = if txInInfoOutRef == txOutRef
then Just i
else go rest
findOwnInput _ = Nothing
Removing Traces
Traces can be expensive especially in terms of script sizes.
It is advisable to use traces during development, but to remove them when deploying your scripts on mainnet.
Traces can be removed via the remove-trace
plugin flag.
Using error
for faster failure
Plutus scripts have access to one impure effect, error
, which immediately terminates the script evaluation and will fail validation.
This failure is very fast, but it is also unrecoverable, so only use it in cases where you want to fail the entire validation if there is a failure.
The Plutus libraries have some functions that fail with error
.
Usually these are given an unsafe
prefix to their name.
For example, PlutusTx.IsData.Class.FromData
parses a value of type Data
, returning the result in a Maybe
value to indicate whether it succeeded or failed; whereas PlutusTx.IsData.Class.UnsafeFromData
does the same but fails with error
.