Optimization techniques for Plutus scripts

Identifying problem areas

In order to identify which parts of the script are responsible for significant resource consumption, you can use the profiling support.

Using strict let-bindings to avoid recomputation

Let-bindings in Haskell are translated to strict let-bindings in Plutus IR, unless they look like they might do computation, in which case they are translated to non-strict let-bindings. This is to avoid triggering effects (e.g. errors) at unexpected times.

However, non-strict let-bindings are less efficient. They do not evaluate their right-hand side immediately, instead they do so where the variable is used. But they are not lazy (evaluating the right-hand side at most once), instead it may be evaluated once each time it is used. You may wish to explicitly mark let-bindings as strict in Haskell to avoid this.

-- This may be compiled non-strictly, which could result
-- in it being evaluated multiple times. However, it will
-- not be evaluated if we take the branch where it is not used.
let x = y + z
in if b then x + x else 1

-- This will be compiled strictly, but this will mean it
-- is evaluated even if we take the branch where it is not used.
let !x = y + z
in if b then x + x else 1

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

Common sub-expression elimination

When several instances of identical expressions exist within a function’s body, it’s worth replacing them with a single (strict) let-bound variable to hold the computed value.

In this example, the cost of storing and retrieving n * c in a single variable is significantly less than recomputing it several times.

let a' = a `divide` n * c
    -- occurrence 1
    b' = b * (n * c)
    -- occurrence 2
    C' = c + (n * c)
in
  foo a' b' c' n

-- Only one occurrence
let !t_mul = n * c
    a' = a `divide` t_mul
    b' = b * t_mul
    c' = c + t_mul
in
  foo a' b' c' n

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.