Skip to content

Commit 045de09

Browse files
authored
Builtin Array example (#7092)
* Builtin Array Example * docs: add upcoming features section for unreleased functionality Create a dedicated documentation section for features under development that are not yet available for use. This provides better organization and clearer messaging about feature availability status. * docs: move BuiltinArray documentation to upcoming features section Relocate BuiltinArray documentation from optimization techniques to its own dedicated page in the upcoming features section. This addresses review feedback about giving the feature more prominence and clearer status messaging. Add comprehensive guidance on when to choose arrays versus lists from the design phase, including performance characteristics and current limitations. Clarify that lookup performance is 206x better than lists but conversion costs must be considered for multiple-lookup scenarios. Include note about future plans to add arrays directly to ScriptContext to avoid conversion overhead.
1 parent 0b2cee1 commit 045de09

File tree

6 files changed

+261
-0
lines changed

6 files changed

+261
-0
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"label": "Upcoming Features",
3+
"position": 75,
4+
"link": {
5+
"type": "generated-index",
6+
"description": "Features that are being developed and will be available in future versions of Plutus."
7+
}
8+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
sidebar_position: 1
3+
---
4+
5+
# Builtin Arrays
6+
7+
:::danger Upcoming Feature
8+
**This is an upcoming feature that is not yet available for use.** The `BuiltinArray` type and related functions are currently under development and will be included in a future version of Plutus. This documentation is provided for preview purposes only.
9+
:::
10+
11+
For multiple lookups by index, `BuiltinArray` provides significantly better performance than lists. The key advantage is in the lookup operations themselves.
12+
13+
**Lookup Performance Comparison:**
14+
A single lookup at index 99 of a 100-element data structure shows that the CPU cost for lookup on a standard Plinth list (`[Integer]`, a sum-of-products type) is **206 times higher** than on a `BuiltinArray`.
15+
16+
**Important Considerations:**
17+
Currently, `BuiltinArray` creation is implemented as conversion from lists, which involves traversing the entire list. This conversion cost should be factored into your performance calculations - the dramatic lookup performance improvement needs to be amortized over multiple lookups to justify the conversion overhead.
18+
19+
As a rule of thumb, if you only need to perform a single lookup, the conversion cost may not be worthwhile. The benefits become apparent when performing several lookups on the same data structure.
20+
21+
**Future Development:**
22+
In future language versions, arrays are planned to be added to the `Data`-encoded `ScriptContext` precisely to avoid these high conversion costs, allowing arrays to be provided directly without requiring conversion from lists.
23+
24+
## Choosing Arrays vs Lists
25+
26+
When designing your data structures, consider your access patterns:
27+
28+
**Choose arrays when:**
29+
- You need multiple index-based lookups (e.g., `arr[42]`, `arr[17]`)
30+
- Your access pattern is primarily random access rather than sequential
31+
- The data structure size is relatively stable after creation
32+
- You're building lookup tables or similar structures
33+
34+
**Choose lists when:**
35+
- You primarily need sequential access (head/tail operations, pattern matching)
36+
- You frequently prepend elements (`:` operator)
37+
- Your access pattern is mostly single-pass iteration
38+
- You're following functional programming patterns that work naturally with lists
39+
40+
**Current limitations:**
41+
Note that you can't always choose arrays "from the start" because data often comes from external sources as lists (like elements in `ScriptContext`). This is why the conversion scenario is currently common, and why future versions plan to provide arrays directly in these contexts.
42+
43+
Functions for working with `BuiltinArray` are available in the `PlutusTx.Builtins` module:
44+
45+
```haskell
46+
import PlutusTx.Builtins
47+
( BuiltinArray
48+
, indexArray
49+
, listToArray
50+
, lengthOfArray
51+
)
52+
```
53+
54+
<details>
55+
<summary>Lookup comparison: SOP List vs. BuiltinList vs. BuiltinArray</summary>
56+
<LiteralInclude file="Example/Builtin/Array/Main.hs" language="haskell" />
57+
58+
Result of the evaluation:
59+
![BuiltinArray Performance Comparison](/code/Example/Builtin/Array/Screenshot.png)
60+
</details>

doc/docusaurus/docs/working-with-scripts/other-optimization-techniques.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ Traces can be expensive especially in terms of script sizes.
128128
It is advisable to use traces during development, but to remove them when deploying your scripts on mainnet.
129129
Traces can be removed via the `remove-trace` plugin flag.
130130

131+
## Using `BuiltinArray` for index-based lookups
132+
133+
For optimizing multiple index-based lookups, see the upcoming [Builtin Arrays](../upcoming-features/builtin-arrays.md) feature.
134+
135+
131136
## Using `error` for faster failure
132137

133138
Plutus scripts have access to one impure effect, `error`, which immediately terminates the script evaluation and will fail validation.

doc/docusaurus/docusaurus-examples.cabal

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,27 @@ executable example-evaluation
7878
-fno-spec-constr -fno-specialise -fno-strictness
7979
-fno-unbox-small-strict-fields -fno-unbox-strict-fields
8080

81+
executable example-builtin-array
82+
import: lang, ghc-version-support, os-support
83+
main-is: Example/Builtin/Array/Main.hs
84+
hs-source-dirs: static/code
85+
default-language: Haskell2010
86+
other-modules: Paths_docusaurus_examples
87+
build-depends:
88+
, base ^>=4.18
89+
, colourista
90+
, plutus-core ^>=1.54
91+
, plutus-tx ^>=1.54
92+
, plutus-tx-plugin ^>=1.54
93+
, plutus-tx:plutus-tx-testlib
94+
, text
95+
96+
ghc-options:
97+
-Wno-missing-signatures -fno-full-laziness
98+
-fno-ignore-interface-pragmas -fno-omit-interface-pragmas
99+
-fno-spec-constr -fno-specialise -fno-strictness
100+
-fno-unbox-small-strict-fields -fno-unbox-strict-fields
101+
81102
executable quickstart
82103
import: lang, ghc-version-support, os-support
83104
main-is: QuickStart.hs
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
{-# LANGUAGE DataKinds #-}
2+
{-# LANGUAGE ImportQualifiedPost #-}
3+
{-# LANGUAGE OverloadedStrings #-}
4+
{-# LANGUAGE RecordWildCards #-}
5+
{-# LANGUAGE ScopedTypeVariables #-}
6+
{-# LANGUAGE TemplateHaskell #-}
7+
{-# OPTIONS_GHC -fplugin PlutusTx.Plugin #-}
8+
{-# OPTIONS_GHC -fplugin-opt PlutusTx.Plugin:target-version=1.1.0 #-}
9+
10+
module Main where
11+
12+
import Colourista (black, blueBg, bold, formattedMessage, greenBg, magentaBg)
13+
import Data.Text (Text)
14+
import Data.Text qualified as Text
15+
import Data.Text.IO qualified as Text
16+
import PlutusCore.Evaluation.Machine.ExBudget (ExBudget (..))
17+
import PlutusCore.Evaluation.Machine.ExMemory (CostingInteger, ExCPU (..), ExMemory (..))
18+
import PlutusTx qualified as Plinth
19+
import PlutusTx.BuiltinList (BuiltinList)
20+
import PlutusTx.BuiltinList qualified as BuiltinList
21+
import PlutusTx.Builtins (BuiltinArray, indexArray, sopListToArray, toOpaque)
22+
import PlutusTx.List qualified as SOP
23+
import PlutusTx.Test.Run.Code (EvalResult (..), displayExBudget, evaluateCompiledCode)
24+
import Text.Printf (printf)
25+
import Unsafe.Coerce (unsafeCoerce)
26+
27+
--------------------------------------------------------------------------------
28+
-- Plinth ----------------------------------------------------------------------
29+
30+
usesSopList :: Plinth.CompiledCode Integer
31+
usesSopList =
32+
$$(Plinth.compile [||lookupByIndex sopListOfInts||])
33+
where
34+
lookupByIndex :: [Integer] -> Integer
35+
lookupByIndex xs = xs SOP.!! 99
36+
37+
usesBuiltinList :: Plinth.CompiledCode Integer
38+
usesBuiltinList =
39+
$$(Plinth.compile [||lookupByIndex (toOpaque sopListOfInts)||])
40+
where
41+
lookupByIndex :: BuiltinList Integer -> Integer
42+
lookupByIndex xs = xs BuiltinList.!! 99
43+
44+
usesArray :: Plinth.CompiledCode Integer
45+
usesArray =
46+
$$(Plinth.compile [||lookupByIndex (sopListToArray sopListOfInts)||])
47+
where
48+
lookupByIndex :: BuiltinArray Integer -> Integer
49+
lookupByIndex xs = indexArray xs 99
50+
51+
sopListConstruction :: Plinth.CompiledCode [Integer]
52+
sopListConstruction = $$(Plinth.compile [||sopListOfInts||])
53+
54+
builtinListConstruction :: Plinth.CompiledCode (BuiltinList Integer)
55+
builtinListConstruction = $$(Plinth.compile [||toOpaque sopListOfInts||])
56+
57+
builtinArrayConstruction :: Plinth.CompiledCode (BuiltinArray Integer)
58+
builtinArrayConstruction = $$(Plinth.compile [||sopListToArray sopListOfInts||])
59+
60+
{- FOURMOLU_DISABLE -}
61+
sopListOfInts :: [Integer]
62+
sopListOfInts =
63+
[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28
64+
,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53
65+
,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78
66+
,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99
67+
]
68+
{- FOURMOLU_ENABLE -}
69+
70+
builtinListOfInts :: BuiltinList Integer
71+
builtinListOfInts = toOpaque sopListOfInts
72+
73+
--------------------------------------------------------------------------------
74+
-- Main ------------------------------------------------------------------------
75+
76+
main :: IO ()
77+
main = do
78+
let sopListConstructionResult = evaluateCompiledCode sopListConstruction
79+
sopListConstructionBudget = evalResultBudget sopListConstructionResult
80+
81+
printHeader greenBg "Lookup in SOP List"
82+
let sopListTotalBudget =
83+
-- Total means construction + lookup
84+
evalResultBudget (evaluateCompiledCode usesSopList)
85+
sopListLookupBudget =
86+
sopListTotalBudget `subtractBudget` sopListConstructionBudget
87+
printBudget sopListLookupBudget
88+
89+
let builtinListConstructionResult =
90+
evaluateCompiledCode builtinListConstruction
91+
builtinListConstructionBudget =
92+
evalResultBudget builtinListConstructionResult
93+
94+
printHeader greenBg "Lookup in Builtin List"
95+
let builtinListLookupEvalResult = evaluateCompiledCode usesBuiltinList
96+
builtinListTotalBudget =
97+
evalResultBudget builtinListLookupEvalResult
98+
builtinListLookupBudget =
99+
builtinListTotalBudget `subtractBudget` builtinListConstructionBudget
100+
printBudget builtinListLookupBudget
101+
102+
let arrayConstructionEvalResult =
103+
evaluateCompiledCode builtinArrayConstruction
104+
arrayConstructionBudget =
105+
evalResultBudget arrayConstructionEvalResult
106+
107+
printHeader greenBg "Lookup in Builtin Array"
108+
let builtinArrayTotalEvalResult = evaluateCompiledCode usesArray
109+
builtinArrayTotalBudget = evalResultBudget builtinArrayTotalEvalResult
110+
builtinArrayLookupBudget =
111+
builtinArrayTotalBudget `subtractBudget` arrayConstructionBudget
112+
printBudget builtinArrayLookupBudget
113+
114+
printHeader magentaBg "SOP List vs. Builtin List"
115+
printPercentage sopListLookupBudget builtinListLookupBudget
116+
117+
printHeader magentaBg "SOP List vs. BuiltinArray"
118+
printPercentage sopListLookupBudget builtinArrayLookupBudget
119+
120+
printHeader magentaBg "BuiltinList vs. BuiltinArray"
121+
printPercentage builtinListLookupBudget builtinArrayLookupBudget
122+
123+
printHeader blueBg "Legend"
124+
putStrLn
125+
"A negative percentage indicates that \
126+
\cost is lower on the right hand side of a comparison."
127+
putStrLn "\n"
128+
129+
--------------------------------------------------------------------------------
130+
-- Helper Functions ------------------------------------------------------------
131+
132+
printHeader :: Text -> Text -> IO ()
133+
printHeader bg x = do
134+
putStrLn ""
135+
formattedMessage [bold, bg, black] (" " <> Text.strip x <> " ")
136+
137+
printBudget :: ExBudget -> IO ()
138+
printBudget = Text.putStrLn . displayExBudget
139+
140+
printPercentage :: ExBudget -> ExBudget -> IO ()
141+
printPercentage oldResult newResult = do
142+
let (cpuOld, memOld) = evalResultToCpuMem oldResult
143+
(cpuNew, memNew) = evalResultToCpuMem newResult
144+
putStr "CPU change: "
145+
putStrLn $ improvementPercentage cpuOld cpuNew
146+
putStr "MEM change: "
147+
putStrLn $ improvementPercentage memOld memNew
148+
where
149+
improvementPercentage :: Double -> Double -> String
150+
improvementPercentage old new =
151+
printf "%+.2f" ((new - old) / old * 100.0) <> " %"
152+
153+
evalResultToCpuMem :: ExBudget -> (Double, Double)
154+
evalResultToCpuMem
155+
ExBudget
156+
{ exBudgetCPU = ExCPU cpu
157+
, exBudgetMemory = ExMemory mem
158+
} = (toDouble cpu, toDouble mem)
159+
where
160+
toDouble :: CostingInteger -> Double
161+
toDouble x = fromIntegral (unsafeCoerce x :: Int)
162+
163+
subtractBudget :: ExBudget -> ExBudget -> ExBudget
164+
subtractBudget
165+
ExBudget{exBudgetCPU = ExCPU cpu1, exBudgetMemory = ExMemory mem1}
166+
ExBudget{exBudgetCPU = ExCPU cpu2, exBudgetMemory = ExMemory mem2} =
167+
ExBudget (ExCPU (cpu1 - cpu2)) (ExMemory (mem1 - mem2))
87.9 KB
Loading

0 commit comments

Comments
 (0)