-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[move][stdlib] Add efficient BigOrderedMap implementation #14753
base: igor/ordered_map
Are you sure you want to change the base?
Conversation
⏱️ 1h 25m total CI duration on this PR
|
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## igor/ordered_map #14753 +/- ##
=================================================
Coverage 57.4% 57.4%
=================================================
Files 859 859
Lines 211663 211663
=================================================
Hits 121527 121527
Misses 90136 90136 ☔ View full report in Codecov by Sentry. |
4ae8d66
to
6e1add6
Compare
3ffc3e4
to
95aaf51
Compare
aptos-move/framework/aptos-stdlib/sources/data_structures/btree_map.move
Outdated
Show resolved
Hide resolved
aptos-move/framework/aptos-stdlib/sources/data_structures/btree_map.move
Outdated
Show resolved
Hide resolved
aptos-move/framework/aptos-stdlib/sources/data_structures/btree_map.move
Outdated
Show resolved
Hide resolved
aptos-move/framework/aptos-stdlib/sources/data_structures/btree_map.move
Outdated
Show resolved
Hide resolved
aptos-move/framework/aptos-stdlib/sources/data_structures/slots_storage.move
Outdated
Show resolved
Hide resolved
aptos-move/framework/aptos-stdlib/sources/data_structures/slots_storage.move
Outdated
Show resolved
Hide resolved
aptos-move/framework/aptos-stdlib/sources/data_structures/slots_storage.move
Outdated
Show resolved
Hide resolved
aptos-move/framework/aptos-stdlib/sources/data_structures/slots_storage.move
Outdated
Show resolved
Hide resolved
aptos-move/framework/aptos-stdlib/sources/data_structures/slots_storage.move
Outdated
Show resolved
Hide resolved
The world with Enums is beautiful 🚀 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
haven't fully review the tree code with some comments first.
/// Destroys the tree if it's empty, otherwise aborts. | ||
public fun destroy_empty<K: store, V: store>(self: BTreeMap<K, V>) { | ||
let BTreeMap::V1 { nodes, root_index, min_leaf_index: _, max_leaf_index: _, inner_max_degree: _, leaf_max_degree: _ } = self; | ||
aptos_std::debug::print(&nodes); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit
aptos-move/framework/aptos-stdlib/sources/data_structures/slots_storage.move
Outdated
Show resolved
Hide resolved
aptos-move/framework/aptos-stdlib/sources/data_structures/slots_storage.move
Outdated
Show resolved
Hide resolved
enum SlotsStorage<T: store> has store { | ||
Simple { | ||
slots: Table<u64, Link<T>>, | ||
new_slot_index: u64, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why do we need the non-reuse version?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if one app does all inserts and removals, they might want to get the refunds back, instead of worrying about amortization
/// Returns a newly allocated vector containing the elements in the range [at, len). | ||
/// After the call, the original vector will be left containing the elements [0, at) | ||
/// with its previous capacity unchanged. | ||
public fun split_off<Element>(self: &mut vector<Element>, at: u64): vector<Element> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This deserves a native functions, so does the next one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
implemented in the stack
@@ -87,7 +87,7 @@ module aptos_std::table { | |||
drop_unchecked_box<K, V, Box<V>>(self) | |||
} | |||
|
|||
public(friend) fun destroy<K: copy + drop, V>(self: Table<K, V>) { | |||
public fun destroy_empty<K: copy + drop, V>(self: Table<K, V>) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC, this function does NOT check the Table is empty...
Could we confirm with @wrwg ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there's no way to check if a table is empty or not, that's why we didn't expose destroy_empty
as a public function
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
/// An iterator to iterate all keys in the BTreeMap. | ||
enum Iterator<K> has copy, drop { | ||
End, | ||
Some { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
name: None
and Next
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's important it is End.
aptos-move/framework/aptos-stdlib/sources/data_structures/btree_map.move
Outdated
Show resolved
Hide resolved
6e1add6
to
9e1d6af
Compare
95aaf51
to
c540209
Compare
} | ||
|
||
// Returns true iff the iterator is an end iterator. | ||
public fun is_end_iter<K: store, V: store>(_tree: &BTreeMap<K, V>, iter: &Iterator<K>): bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this feels more like inline func than actual func?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cannot be done publicly, as enum variants are not public. also some implementation might actually need to do something here.
@@ -87,7 +87,7 @@ module aptos_std::table { | |||
drop_unchecked_box<K, V, Box<V>>(self) | |||
} | |||
|
|||
public(friend) fun destroy<K: copy + drop, V>(self: Table<K, V>) { | |||
public fun destroy_empty<K: copy + drop, V>(self: Table<K, V>) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there's no way to check if a table is empty or not, that's why we didn't expose destroy_empty
as a public function
aptos-move/framework/aptos-stdlib/sources/data_structures/btree_map.move
Outdated
Show resolved
Hide resolved
Inner { | ||
// The max key of its child, or the key of the current node if it is a leaf node. | ||
max_key: K, | ||
// The node index of it's child, or NULL_INDEX if the current node is a leaf node. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This enum is somehow confusing to me. Could you draw a ascii diagram to give an example?
NULL_INDEX if the current node is a leaf node
If this is a leaf node, it should be Leaf
, right?
public fun new_with_config<K: store, V: store>(inner_max_degree: u16, leaf_max_degree: u16, reuse_slots: bool, num_to_preallocate: u64): BTreeMap<K, V> { | ||
assert!(inner_max_degree == 0 || inner_max_degree >= DEFAULT_INNER_MIN_DEGREE, E_INVALID_PARAMETER); | ||
assert!(leaf_max_degree == 0 || leaf_max_degree >= DEFAULT_LEAF_MIN_DEGREE, E_INVALID_PARAMETER); | ||
let nodes = if (reuse_slots) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any reason we don't want to reuse slots?
let new_node_children = children.split_off(target_size - 1); | ||
children.insert(l, child); | ||
new_node_children | ||
} else { | ||
children.insert(l, child); | ||
children.split_off(target_size) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why do we need two cases? Just
children.insert(l, child);
children.split_off(target_size)
is good. unless it's for perf.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it was for perf. since it is only on rebalancing, cleaned it up.
let BTreeMap::V1 { nodes, root_index, min_leaf_index: _, max_leaf_index: _, inner_max_degree: _, leaf_max_degree: _ } = self; | ||
aptos_std::debug::print(&nodes); | ||
nodes.remove(root_index).destroy_empty_node(); | ||
nodes.destroy_empty(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As we discussed, how to gc?
self.nodes.borrow_mut(*prev).next = left_node_index; | ||
}; | ||
|
||
if (!*is_leaf) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
comments... update the non-leaf children parents pointer to the left node, which was the original node with new index.
let left_node_slot = self.nodes.create_transient_slot(); | ||
let left_node_index = left_node_slot.get_index(); | ||
right_node.next = *next; | ||
*next = node_index; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please comment
// node_index == right_node_index;
}; | ||
|
||
// # of children in the current node exceeds the threshold, need to split into two nodes. | ||
let (right_node_slot, node) = self.nodes.transiently_remove(node_index); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not reuse this node as the left node?
// The brother node has enough elements, borrow an element from the brother node. | ||
brother_size = brother_size - 1; | ||
if (brother_index == next) { | ||
let borrowed_element = brother_children.remove(0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we need a native remove too.
}; | ||
let borrowed_max_key = borrowed_element.max_key; | ||
children.push_back(borrowed_element); | ||
current_size = current_size + 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unnecessary? just make current_size - 1
at the following line?
9e1d6af
to
0d2ea8f
Compare
c540209
to
bde797a
Compare
bde797a
to
bd71740
Compare
563d169
to
d58cde2
Compare
bd71740
to
09cdccf
Compare
615a239
to
c818391
Compare
6cd4fcf
to
79ceaf7
Compare
c818391
to
6df2443
Compare
79ceaf7
to
c13fc13
Compare
6df2443
to
81821f4
Compare
c13fc13
to
ed8de0e
Compare
Description
Having efficient and concurrent large-sized "Map" and "OrderedMap" implementations is useful across variety of needs.
Current SmartTable implementation has various limitations, from it being sequential, to it not having smart ways to deal with collisions. It cannot be improved - as the structs are unmodifiable, and so should be deprecated fully.
In this PR:
How Has This Been Tested?
provided extensive unit tests. will probably consolidate and remove some before committing (to not make CI slower/more expensive), but for development those were useful.
For performance, we measured two things.
At large scale
Most relevantly - we measured performance at scale, in comparison to SmartTable. So adding 1M keys into each, with making entries be 4KB on each. we get:
This shows BigOrderedMap being more storage-efficient, especially when keys/values are small, due to SmartTable storing hash in addition to key,value. It is also more performant even on 1M keys, when we are optimizing for storage costs (i.e. more elements in a single bucket). As we reduce the size of the bucket, SmartTable becomes more competitive, but the storage costs increase significantly.
Note: Test is compared on the single thread to compare apples to apples, as SmartTable has no parallelism. BigOrderedMap is parallel, and running on more threads gives order of magnitude higher throughput.
At small scale
We also measured performance at small scale, and how much overhead is it to use BigOrderedMap, instead of just OrderedMap, when it is unknown if data will be too large to be stored in a single resource.
Here we measure nanoseconds taken for a single pair of insert + remove operation, into a map of varied size.
Here we can see that inlining of the root node makes the overhead be less than 2 times, and even splitting small maps to achieve higher parallelism - keeps the overhead reasonable (i.e. another 2-3 times). But in all cases it scales extremely well as dataset increases in size.
Key Areas to Review
Type of Change
Which Components or Systems Does This Change Impact?
Checklist