-
Notifications
You must be signed in to change notification settings - Fork 247
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
Keys
iterator of the store::UnorderedMap
has a huge performance drop after removing elements
#1134
Comments
Another problem is that UPD: created an issue #1135 to track it there. |
I found an enhancement ticket outlining the same issue: #990. There, However, the problem lies in the fact that running through the
|
@fadeevab From what I can tell, the old @austinabell Could you help to shed some light on the motivation for using FreeList over Vector for new store collections? Did you have any solution for dealing with iteration/slicing efficiently? |
It's a tradeoff, yes sth like skip iteration will be worse, but for removals for example, the old implementation does a lot of reads and writes to have to update all new indices for any element removed. (Note you can minimize the amount of reads/writes of this old implementation slightly, but all comes at the cost of breaking changes, which I wouldn't recommend) There's no perfect solution, and decisions were made to optimize the most commonly used patterns. Possible those are incorrect or prohibitive in real world cases. Also unclear why skip iteration is an important pattern for an unordered map, the ordering isn't stable. You just want to load values out of the maps in chunks or something? |
@frol Indeed,
Anyways, |
Well, the order is stable if one makes several requests at the same block height and the most common scenario is to fetch the data in pages, and it seems that the current implementation degrades the performance significantly turning it into a storage-inefficient LookupMap. |
@fadeevab What would be the ideal solution? I don't have the capacity to dive into it, do you? |
The challenges I see:
|
This seems like not too bad of a solution:
What do you think @frol ? |
@ruseinov The issue is that Quick ugly solution - implement .nth and if FreeList is defragmented, make it efficient, otherwise fallback to slow version. But I fear that there might be situation when defrag won't be able to complete within the gas limit and then what? And still, it sounds like a major footgun and you need to know how to heal it... |
Yeah, I hear you. We could implement an alternative solution to defrag, but that'd require a migration that spans several blocks, which is a pain for several reasons. Though that in combination with the new logic would eventually lead to better performance for old contracts and good performance on fresh ones out of the box. We could also make sure that the collection is using the old logic unless it's been defragmented, then a migration will be a soft requirement. What are the alternatives.
|
Seems like the whole point of the redesigned @frol I imagine someone may need such a map for small collections: quick removals, periodic defrags in a single block, etc. From my perspective, the ideal solution would be renaming the On a side note, I found a nice improvement to the |
I'm not sure what the purpose of this would be, if you'd want to clean the map randomly - might as well clear it, right? Because it means that data it stores does not matter, unless I'm missing the point here.
We might need to revisit this to see exactly how much faster it ends up being and what purpose it serves. I reckon if we're talking small collections - there are other collections that could be used. If the collection is big and it becomes fragmented due to removes - then what's the point of this optimisation? Let's imagine some scenarios:
Correct me if I'm wrong about the above.
Could be done, but then that means that for older contracts they'd need to re-integrate and rename + still do a migration of some sort. Plus if we want to use the same name/namespace for the new collection - some of that stuff could get lost in translation and things won't get migrated at all, potentially causing the data to be either corrupt or fragmented. There's another truth, all the contracts that currently use |
@ruseinov You miss the point that it can't clear more than ~2-3k elements per block. The same problem as with |
I have mentioned about defrag that it can be limited to max consumed gas and do partial defrag. Is it not possible to just drop the whole data structure without actually having to delete each element? In theory that should be cheaper, depending on how those are stored. |
@ruseinov Then we miss the following real use case: I want to select 1k random elements, process them and drop. Let's not discuss |
Yes, let's not do this here. Please create an issue for that. Though using a collection like that as a queue with random reads makes very little sense to me, I can't think of a case where it's useful. Unless the queue is guaranteed to be exhausted, then it's probably small enough to use a different collection altogether. Let's get back on track and figure out how to fix the original issue. |
I've been thinking about this more, I guess we should go with defrag the element on remove approach. Here's why:
I still have to investigate performance implications of that though. And there's still a question of defrag migrations, I'd like to hear from somebody who has migration experience to see if there is a way to do a multi-block/partial migration trick. |
@ruseinov The fastest "defrag on remove" approach is "swap remove" which is already implemented in I believe that either someone really needed often fast removals (though you think it makes no sense) and we have to keep the current structure under another name (as I mentioned here #1134 (comment)), or it was unfortunate optimisation attempt which led to more optimizations like |
Indeed, I didn't realize that defrag has already been implemented. Looking at all of this now I'd say it's probably easier to deprecate this altogether ( If we opt for a rename - it's probably useful to mention trade-offs directly in module documentation. It is mentioned in the comment to |
I'll raise a draft PR with a rename an additional comments. |
@ruseinov @fadeevab I feel that near-sdk-rs/near-sdk/src/store/unordered_map/mod.rs Lines 19 to 21 in e78e06a
If you cannot iterate after a certain size, it is pointless to use the collection, and So I see it in the following way:
We should provide That said, I suggest we just mark the |
Right, I would probably go for versioning the module name. It's really a pity that we can't type alias our way through it. |
I agree, and it's a pity that it happened. By the way,
Sad to lose the good package name, I would consider renaming the types, e.g. into |
It's a matter of taste, I'd prefer a concise name for the struct and a version in module path (e.g. |
@ruseinov |
@fadeevab There are definitely pros and cons to various naming:
I like |
@frol Cool, I'm keen to vote for this naming because it was my idea and because I made a room for myself to change my mind 😄
|
There is an alternative, we could move the old The issue I see with Perhaps it's a good idea to think about a different name altogether. But then it would be great to rename the |
I'm assuming that we want to take this further and implement an actual map that will be optimised for iteration. Should a separate issue be created for that? It would be nice to flesh out how exactly this should work though. |
@ruseinov well, it tells us what we literally overlooked last time: iterableness. And it's a map. |
Nope, all that it tells us is that the map is iterable, as in could be iterated upon. Frangible* tells us that a collection gets fragmented, that's informative. Why? Because all the others aren't. That's my personal opinion though, feel free to ignore it. What's more important is what are the next steps here. Naming is difficult and can be improved upon going forward. Let's flesh out the logic for a new map if it's at all needed. We already have a |
The whole idea of struct {
accounts: UnorderedMap<AccountId, UnorderedMap<TokenId, Balance>>
}
fn update(&mut self) {
let account_tokens = self.accounts.get("frol.near").unwrap();
account_tokens.insert("tkn", 10);
// WARNING: Unless you do the following line, you get your state in a broken state since the key-value in account_tokens is updated, but the length is not updated
self.accounts.insert("frol.near", account_tokens);
}
|
|
I'm going to throw some questions here as I'm trying to understand the current situation. Any reason those collections ( How should the users know to avoid this footgun if both collections store their data on a trie and don't explicitly mention these differences? I'm happy to take a stab at |
It is just a lack of testing for store types. The goal was to deprecate old collections once store is tested. It would be awesome if you can take a lead here! |
Also, methods of |
@fadeevab @frol
Something else I'm missing? |
@ruseinov yes, looks good... |
@ruseinov Looks good to me |
Yeah, I encountered similar problem 1.5 year ago when I started to use the "new" |
I really thing we should take an advantage of the fact that we are using a tree for store and enable prefix scan rather adding limitations and making our life harder. I proposed this in: near/NEPs#515 In fact every DB is using a kind of a tree, so even if we will do commitment with a different structure, the DB will allow us to do prefix scan. TBH, all good DBs provide prefix scan feature. |
It does not seem relevant to the current task, however a separate issue can be created. @fadeevab @frol I've been looking into "migrating" |
Problem
store::UnorderedMap
O(1000)
time instead ofO(100)
expected.Trying to take just a constant number of keys gives an inconsistent performance with increasing gas consumption 3TGas -> 10 TGas -> 20 TGas ->....., while removing bunches of elements from the map.
Cause
FreeList
is now used for the keys in thestore::UnorderedMap
, there is an iterator that goes through the "holes" in the vector of keys which never shrinks when elements are removed.Example
Here, iterating through 100 keys after removals gives
0.5 TGas
consumption instead of0.04 TGas
if 100 elements were just inserted.0.5 TGas
- 100 keys, after removals.0.04 TGas
- 100 keys, no removals, just inserts.x10 performance drop.
It's worse when there are thousands of elements.
The text was updated successfully, but these errors were encountered: