Skip to content

Commit 0d603e4

Browse files
authoredMar 26, 2025··
Add Maps to documentation (#856)
* Add Maps to documentation * addressing review comments * distinguishing between resource and storage slot --------- Co-authored-by: Igor <igor-aptos@users.noreply.github.com>
1 parent 9e57f09 commit 0d603e4

File tree

4 files changed

+203
-195
lines changed

4 files changed

+203
-195
lines changed
 

‎apps/nextra/next.config.mjs

+5
Original file line numberDiff line numberDiff line change
@@ -1866,6 +1866,11 @@ export default withBundleAnalyzer(
18661866
destination: "/en/network/faucet",
18671867
permanent: true,
18681868
},
1869+
{
1870+
source: "/en/build/smart-contracts/smart-table",
1871+
destination: "/en/build/smart-contracts/maps",
1872+
permanent: true,
1873+
},
18691874
],
18701875
}),
18711876
);

‎apps/nextra/pages/en/build/smart-contracts/_meta.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ export default {
7575
"smart-vector": {
7676
title: "Smart Vector",
7777
},
78-
"smart-table": {
79-
title: "Smart Table",
78+
maps: {
79+
title: "Maps",
8080
},
8181
"---examples---": {
8282
type: "separator",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
---
2+
title: "Maps"
3+
---
4+
5+
# Overview
6+
7+
There are multiple different implementations of key-value maps inside the framework, suited for different usecases.
8+
We will go over their differences and similarities, and how to choose which one to use.
9+
10+
## Aptos Blockchain performance and gas cost considerations
11+
12+
Aptos Blockchain state is kept in **storage slots**.
13+
Furthermore, transaction performance and gas cost is heavily influenced by how these **slots** are read and written.
14+
Breaking down the gas costs further, we have:
15+
1. Storage fee, which are determined by the number and size of **slots** (i.e., writing to a new **slot** incurs the highest storage fee, whereas deleting an existing **slot** provides the largest refund.)
16+
2. IO gas costs —generally much lower— which depend on the number and size of resources read and modified.
17+
3. execution gas costs are based on the computation needed, and are generally in the similar scale as io gas costs.
18+
19+
Transactions that modify the same **slot** cannot be executed concurrently (with some exceptions, like aggregators and resources as a part of the same resource group), as they conflict with one another.
20+
21+
One useful analogy is thinking about each **slot** being a file on a disk,
22+
then performance of smart contract would correlate well to a program that
23+
operates on files in the same way.
24+
25+
## Different Map implementations
26+
27+
| Implementation | Size Limit | Storage Structure | Key Features |
28+
|--------------------|------------|------------------|--------------|
29+
| `OrderedMap` | Bounded (fits in a single **slot**) | Stored entirely within the resource that contains it | Supports ordered access (front/back, prev/next), implemented as sorted vector, but operations are effectively O(log(n)) due to internal optimizations |
30+
| `Table` | Unbounded | Each (key, value) stored in a separate **slot** | Supports basic operations, like `add`, `remove`, `contains`, but **not iteration**, and **cannot be destroyed**; useful for large/unbounded keys/values and where high-concurrency is needed |
31+
| `TableWithLength` | Unbounded | same as `Table` | Variant of `Table`, with additional length tracking, which adds `length`, `empty`, and `destroy_empty` methods; Adding or removing elements **cannot** be done concurrently, modifying existing elements can. |
32+
| `BigOrderedMap` | Unbounded | Combines multiple keys into a single **slot**, initially stored within resource that contains it, and grows into multiple **slots** dynamically | Implemented as B+ tree; **opportunistically concurrent** for non-adjacent keys; supports ordered access (front/back, prev/next); configurable node capacities to balance storage and performance |
33+
34+
Note:
35+
- `SimpleMap` has been deprecated, and replaced with `OrderedMap`.
36+
- `SmartTable` has been deprecated, and replaced with `BigOrderedMap`.
37+
38+
#### Performance comparison
39+
40+
We measured performance at small scale, measuring microseconds taken for a single pair of `insert` + `remove` operation, into a map of varied size.
41+
42+
| num elements | OrderedMap | BigOrderedMap max_degree>10000 | BigOrderedMap max_degree=16 |
43+
|--------------|------------|---------------------------|-----------------------------|
44+
| 10 | 65 | 123 | 123 |
45+
| 100 | 85 | 146 | 455 |
46+
| 1000 | 105 | 168 | 567 |
47+
| 10000 | 142 | 210 | 656 |
48+
49+
You can see that overhead of `BigOrderedMap` compared to `OrderedMap`, when both are in the single **slot**, is around 1.5-2x.
50+
So you can generally used `BigOrdredMap` when it is unknown if data will be too large to be stored in a single **slot**.
51+
52+
## Common map operations:
53+
54+
Most maps above support the same set of functions (for actual signatures and restrictions, check out the corresponding implementations):
55+
56+
#### Creating Maps
57+
58+
- `new<K, V>(): Self`: creates an empty map
59+
60+
#### Destroying Maps
61+
62+
- `destroy_empty<K, V>(self: Self<K, V>)`: Destroys an empty map. (**not** supported by `Table`)
63+
- `destroy<K, V>(self: Self<K, V>, dk: |K|, dv: |V|)`: Destroys a map with given functions that destroy correponding elements. (**not** supported by `Table` and `TableWithLength`)
64+
65+
#### Managing Entries
66+
67+
- `add<K, V>(self: &mut Self<K, V>, key: K, value: V)`: Adds a key-value pair to the map.
68+
- `remove<K, V>(self: &mut Self<K, V>, key: K): V`: Removes and returns the value associated with a key.
69+
- `upsert<K, V>(self: &mut Self<K, V>, key: K, value: V): Option<V>`: Inserts or updates a key-value pair.
70+
- `add_all<K, V>(self: &mut Self<K, V>, keys: vector<K>, values: vector<V>)`: Adds multiple key-value pairs to the map. (**not** supported by `Table` and `TableWithLength`)
71+
72+
#### Retrieving Entries
73+
74+
- `contains<K, V>(self: &Self<K, V>, key: &K): bool`: Checks whether key exists in the map.
75+
- `borrow<K, V>(self: &Self<K, V>, key: &K): &V`: Returns an immutable reference to the value associated with a key.
76+
- `borrow_mut<K: drop, V>(self: &mut Self<K, V>, key: K): &mut V`: Returns a mutable reference to the value associated with a key.
77+
(`BigOrderedMap` only allows `borrow_mut` when value type has a static constant size, due to modification being able to break it's invariants otherwise. Use `remove()` and `add()` combination instead)
78+
79+
#### Order-dependant functions
80+
81+
These set of functions are only implemented by `OrderedMap` and `BigOrderedMap`.
82+
83+
- `borrow_front<K, V>(self: &Self<K, V>): (&K, &V)`
84+
- `borrow_back<K, V>(self: &Self<K, V>): (&K, &V)`
85+
- `pop_front<K, V>(self: &mut Self<K, V>): (K, V)`
86+
- `pop_back<K, V>(self: &mut Self<K, V>): (K, V)`
87+
- `prev_key<K: copy, V>(self: &Self<K, V>, key: &K): Option<K>`
88+
- `next_key<K: copy, V>(self: &Self<K, V>, key: &K): Option<K>`
89+
90+
#### Utility Functions
91+
92+
- `length<K, V>(self: &Self<K, V>): u64`: Returns the number of entries in the map. (not supported by `Table`)
93+
94+
#### Traversal Functions
95+
96+
These set of functions are not implemented by `Table` and `TableWithLength`.
97+
98+
- `keys<K: copy, V>(self: &Self<K, V>): vector<K>`
99+
- `values<K, V: copy>(self: &Self<K, V>): vector<V>`
100+
- `to_vec_pair<K, V>(self: Self<K, V>): (vector<K>, vector<V>)`
101+
- `for_each_ref<K, V>(self: &Self<K, V>, f: |&K, &V|)`
102+
103+
- `to_ordered_map<K, V>(self: &BigOrderedMap<K, V>): OrderedMap<K, V>`: Converts `BigOrderedMap` into `OrderedMap`
104+
105+
## Example Usage
106+
107+
### Creating and Using a OrderedMap
108+
109+
```move filename="map_usage.move"
110+
module 0x42::map_usage {
111+
use aptos_framework::ordered_map;
112+
113+
public entry fun main() {
114+
let map = ordeded_map::new<u64, u64>();
115+
map.add(1, 100);
116+
map.add(2, 200);
117+
118+
let length = map.length();
119+
assert!(length == 2, 0);
120+
121+
let value1 = map.borrow(&1);
122+
assert!(*value1 == 100, 0);
123+
124+
let value2 = map.borrow(&2);
125+
assert!(*value2 == 200, 0);
126+
127+
let removed_value = map.remove(&1);
128+
assert!(removed_value == 100, 0);
129+
130+
map.destroy_empty();
131+
}
132+
}
133+
```
134+
135+
## Additional details for `BigOrderedMap`
136+
137+
Its current implementation is B+ tree, which is chosen as it is best suited for the onchain storage layout - where the majority of cost comes from loading and writing to storage items, and there is no partial read/write of them.
138+
139+
Implementation has few characteristics that make it very versatile and useful across wide range of usecases:
140+
141+
- When it has few elements, it stores all of them within the resource that contains it, providing comparable performance to OrderedMap itself, while then dynamically growing to multiple resources as more and more elements are added
142+
- It reduces amount of conflicts: modifications to a different part of the key-space can be generally done concurrently, and it provides knobs for tuning between concurrency and size
143+
- All operations have guaranteed upper-bounds on performance (how long they take, as well as how much execution and io gas they consume), allowing for safe usage across a variety of use cases.
144+
- One caveat, is refundable storage fee. By default, operation that requires map to grow to more resources needs to pay for storage fee for it. Implementation here has an option to pre-pay for storage slots, and to reuse them as elements are added/removed, allowing applications to achieve fully predictable overall gas charges, if needed.
145+
- If key/value is within the size limits map was configured with, inserts will never fail unpredictably, as map internally understands and manages maximal **slot** size limits.
146+
147+
### `BigOrderedMap` structure
148+
149+
`BigOrderedMap` is represented as a tree, where inner nodes split the "key-space" into separate ranges for each of it's children, and leaf nodes contain the actual key-value pairs.
150+
Internally it has `inner_max_degree` representing largest number of children an inner node can have, and `leaf_max_degree` representing largest number of key-value pairs leaf node can have.
151+
152+
#### Creating `BigOrderedMap`
153+
154+
Because it's layout affects what can be inserted and performance, there are a few ways to create and configure it:
155+
156+
- `new<K, V>(): Self<K, V>`: Returns a new `BigOrderedMap` with the default configuration. Only allowed to be called with constant size types. For variable sized types, another constructor is needed, to explicitly select automatic or specific degree selection.
157+
- `new_with_type_size_hints<K, V>(avg_key_bytes: u64, max_key_bytes: u64, avg_value_bytes: u64, max_value_bytes: u64): Self<K, V>`: Returns a map that is configured to perform best when keys and values are of given `avg` sizes, and guarantees to fit elements up to given `max` sizes.
158+
- `new_with_config<K, V>(inner_max_degree: u16, leaf_max_degree: u16, reuse_slots: bool): Self<K, V>`: Returns a new `BigOrderedMap` with the provided max degree consts (the maximum # of children a node can have, both inner and leaf). If 0 is passed for either, then it is dynamically computed based on size of first key and value, and keys and values up to 100x times larger will be accepted.
159+
If non-0 is passed, sizes of all elements must respect (or their additions will be rejected):
160+
- `key_size * inner_max_degree <= MAX_NODE_BYTES`
161+
- `entry_size * leaf_max_degree <= MAX_NODE_BYTES`
162+
163+
`reuse_slots` means that removing elements from the map doesn't free the storage slots and returns the refund.
164+
Together with `allocate_spare_slots`, it allows to preallocate slots and have inserts have predictable gas costs.
165+
(otherwise, inserts that require map to add new nodes, cost significantly more, compared to the rest)
166+
167+
## Source Code
168+
169+
- [ordered_map.move](https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-framework/sources/datastructures/ordered_map.move)
170+
- [table.move](https://github.com/aptos-labs/aptos-core/blob/6f5872b567075fe3615e1363d35f89dc5eb45b0d/aptos-move/framework/aptos-stdlib/sources/table.move)
171+
- [table_with_length.move](https://github.com/aptos-labs/aptos-core/blob/6f5872b567075fe3615e1363d35f89dc5eb45b0d/aptos-move/framework/aptos-stdlib/sources/table.move)
172+
- [big_ordered_map.move](https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-framework/sources/datastructures/big_ordered_map.move)
173+
174+
## Additional details of (deprecated) SmartTable
175+
176+
The Smart Table is a scalable hash table implementation based on linear hashing.
177+
This data structure aims to optimize storage and performance by utilizing linear hashing, which splits one bucket at a time instead of doubling the number of buckets, thus avoiding unexpected gas costs.
178+
Unfortunatelly, it's implementation makes every addition/removal be a conflict, making such transactions fully sequential.
179+
The Smart Table uses the SipHash function for faster hash computations while tolerating collisions. Unfortunatelly, this also means that collisions are predictable, which means that if end users can control the keys being inserted, it can have large number of collisions in a single bucket.
180+
181+
### SmartTable Structure
182+
183+
The `SmartTable` struct is designed to handle dynamic data efficiently:
184+
185+
- `buckets`: A table with a length that stores vectors of entries.
186+
- `num_buckets`: The current number of buckets.
187+
- `level`: The number of bits representing `num_buckets`.
188+
- `size`: The total number of items in the table.
189+
- `split_load_threshold`: The load threshold percentage that triggers bucket splits.
190+
- `target_bucket_size`: The target size of each bucket, which is not strictly enforced.
191+
192+
### SmartTable usage examples
193+
194+
- [Move Spiders Smart Table](https://movespiders.com/courses/modules/datastructures/lessonId/7)
195+
- [Move Spiders Querying Smart Table via FullNode APIs](https://movespiders.com/courses/modules/datastructures/lessonId/9)
196+
- [Move Spiders Querying Smart Table via View Function](https://movespiders.com/courses/modules/datastructures/lessonId/10)

‎apps/nextra/pages/en/build/smart-contracts/smart-table.mdx

-193
This file was deleted.

0 commit comments

Comments
 (0)
Please sign in to comment.