Skip to content
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

Simple Mapping type improvements #979

Merged
merged 38 commits into from
Nov 9, 2021

Conversation

Robbepop
Copy link
Collaborator

@Robbepop Robbepop commented Oct 23, 2021

Based on @HCastano 's #946 PR.

This fork of the above PR improves the Mapping type even further which reduces the ERC-20 example smart contract Wasm file size to 10.8kB so far.

Implementation Notes

  • The ink! codegen does not automatically generate SpreadAllocate implementations of its #[ink(storage)] struct and at this point it is even undecided if that's a good idea. Therefore we currently are required to implement SpreadAllocate manually in the ERC-20 example contract.
  • The ERC-20 new constructor as well as the new_init method seem a bit verbose but we are working on some improvements to make this less of a deal.
  • The EncodeLike trait usage in the Mapping type allows us to use &(&K1, &K2) in the Mapping::{get, insert} methods instead of just &(K1, K2) which saves us a lot of Wasm file size overhead with respect to big value types such as Balance and especially AccountId.
  • To avoid dealing with big value types helper methods have been implemented in the ERC-20 example contract such as allowance_impl and balance_of_impl that take and forward their arguments by reference instead of by value.

HCastano and others added 30 commits October 23, 2021 11:00
This reverts commit cede033.

Using `RefCell`/`UnsafeCell` doesn't reduce the contract size more than
what we have now, and it introduced `unsafe` code. We believe the
limiting factor here is the `Key` type definition anyways.
Copy link
Collaborator

@ascjones ascjones left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

As you say the constructor could do with some improvement, but size improvements good.

/// # Note
///
/// Prefer to call this method over `balance_of` since this
/// works using references which are more efficient in Wasm.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder whether that's another ra lint we could add in cargo-contract, to prefer references as keys for a Mapping.

Copy link
Contributor

@HCastano HCastano left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, have you been able to successfully deploy the ERC-20 contract to a local chain?

examples/erc20/lib.rs Show resolved Hide resolved

/// Returns the amount which `spender` is still allowed to withdraw from `owner`.
///
/// Returns `0` if no allowance has been set `0`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Returns `0` if no allowance has been set `0`.
/// Returns `0` if no allowance has been set.

/// works using references which are more efficient in Wasm.
#[inline]
fn balance_of_impl(&self, owner: &AccountId) -> Balance {
self.balances.get(owner).unwrap_or_default()
}

/// Returns the amount which `spender` is still allowed to withdraw from `owner`.
///
/// Returns `0` if no allowance has been set `0`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Returns `0` if no allowance has been set `0`.
/// Returns `0` if no allowance has been set.

examples/erc20/lib.rs Show resolved Hide resolved
examples/erc20/lib.rs Show resolved Hide resolved
///
/// This key is a combination of the `Mapping`'s internal `offset_key`
/// and the user provided `key`.
fn storage_key<Q>(&self, key: Q) -> Key
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fn storage_key<Q>(&self, key: Q) -> Key
#[inline]
fn storage_key<Q>(&self, key: Q) -> Key

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have you benchmarked that this yields an improvement?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we don't need to specify the inline attribute. Because the compiler will decide it based on the optimization flag(in our case it reduces the size of the code). So compiler better knows what better to inline to reduce the size of the contract=)
https://nnethercote.github.io/perf-book/inlining.html

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just tried now, and there are no changes to the Wasm size. However, there are also no changes if I remove the inline's from insert() and get(), so maybe we should get rid of those as well?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if the compiler also takes into account that passing large types (> 64bit) is very expensive in wasm. Ideally, Q would be a reference.

@HCastano HCastano changed the base branch from master to hc-simple-mapping-primitive November 4, 2021 17:38
Q: scale::Encode,
V: core::borrow::Borrow<R>,
R: PackedLayout,
Q: scale::EncodeLike<K>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can I think of EncodeLike sort-of like EncodeLike = Borrow + Encode?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EncodeLike is a purely marker trait coming from the parity-scale-codec crate. Please go to the crates docs to read more about its general use.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read the docs, and I think my question is still valid 😄

I think the semantics of a type being able to be SCALE encoded as another type (EncodeLike) vs. being able to be borrowed as another type (Borrow) are slightly different, which is why I asked

@@ -55,24 +60,37 @@ mod erc20 {
/// The ERC-20 result type.
pub type Result<T> = core::result::Result<T, Error>;

impl SpreadAllocate for Erc20 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a note here that this is intended to only be temporary and will (hopefully) be replaced by #995?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is better to merge this PR after merging the one PR that improves the SpreadLayout traits.

examples/erc20/lib.rs Show resolved Hide resolved
@HCastano HCastano changed the title [WIP] Simple Mapping type improvements Simple Mapping type improvements Nov 4, 2021
@HCastano HCastano mentioned this pull request Nov 4, 2021
@HCastano HCastano merged commit b954162 into hc-simple-mapping-primitive Nov 9, 2021
@HCastano HCastano deleted the hc-rf-mapping-type branch November 9, 2021 15:44
HCastano added a commit that referenced this pull request Nov 15, 2021
* Add `Mapping` storage collection

* Implement `insert` and `get` for `Mapping`

* Implement `SpreadLayout` for `Mapping`

* Fix typo

* Add some basic tests

* Fix some documentation formatting

* Use `PackedLayout` as trait bound instead of `Encode/Decode`

* Avoid using low level `ink_env` functions when interacting with storage

* RustFmt

* Appease Clippy

* Only use single `PhantomData` field

* Change `get` API to take reference to `key`

* Implement `TypeInfo` and `StorageLayout` for `Mapping`

* Properly gate `TypeInfo` and `StorageLayout` impls behind `std`

* Replace `HashMap` with `Mapping` in ERC-20 example

* Return `Option` from `Mapping::get`

* Update ERC-20 to handle `Option` returns

* Change `get` and `key` to use `Borrow`-ed values

* Add `Debug` and `Default` implementations

* Proper spelling

* Change `insert` to only accept borrowed K,V pairs

* Update ERC-20 example accordingly

* Make more explicit what each `key` is referring to

* Try using a `RefCell` instead of passing `Key` around

* Try using `UnsafeCell` instead

* Revert "Try using a `RefCell` instead of passing `Key` around"

This reverts commit cede033.

Using `RefCell`/`UnsafeCell` doesn't reduce the contract size more than
what we have now, and it introduced `unsafe` code. We believe the
limiting factor here is the `Key` type definition anyways.

* Clean up some of the documentation

* Simple Mapping type improvements (#979)

* Add `Mapping` storage collection

* Implement `insert` and `get` for `Mapping`

* Implement `SpreadLayout` for `Mapping`

* Fix typo

* Add some basic tests

* Fix some documentation formatting

* Use `PackedLayout` as trait bound instead of `Encode/Decode`

* Avoid using low level `ink_env` functions when interacting with storage

* RustFmt

* Appease Clippy

* Only use single `PhantomData` field

* Change `get` API to take reference to `key`

* Implement `TypeInfo` and `StorageLayout` for `Mapping`

* Properly gate `TypeInfo` and `StorageLayout` impls behind `std`

* Replace `HashMap` with `Mapping` in ERC-20 example

* Return `Option` from `Mapping::get`

* Update ERC-20 to handle `Option` returns

* Change `get` and `key` to use `Borrow`-ed values

* Add `Debug` and `Default` implementations

* Proper spelling

* Change `insert` to only accept borrowed K,V pairs

* Update ERC-20 example accordingly

* Make more explicit what each `key` is referring to

* Try using a `RefCell` instead of passing `Key` around

* Try using `UnsafeCell` instead

* Revert "Try using a `RefCell` instead of passing `Key` around"

This reverts commit cede033.

Using `RefCell`/`UnsafeCell` doesn't reduce the contract size more than
what we have now, and it introduced `unsafe` code. We believe the
limiting factor here is the `Key` type definition anyways.

* Clean up some of the documentation

* adjust the Mapping type for the new SpreadAllocate trait

* adjust ERC-20 example for changes in Mapping type

* remove commented out code

* add doc comment to new_init

* make it possible to use references in more cases with Mapping

* use references in more cases for ERC-20 example contract

* remove unnecessary references in Mapping methods

* refactor/improve pull_packed_root_opt utility method slightly

* fix ERC-20 example contract

The problem with *self.total_supply is that it may implicitly read from storage in case it has not yet read a value from storage whereas Lazy::set just writes the value to the Lazy instance.

Co-authored-by: Hernando Castano <hernando@hcastano.com>
Co-authored-by: Hernando Castano <HCastano@users.noreply.github.com>

* Use new `initialize_contract()` function

* Derive `SpreadAllocate` for `ink(storage)` structs

* Stop manually implementing SpreadAllocate for ERC-20

* Stop implementing `SpreadAllocate` in the storage codegen

* Derive `SpreadAllocate` manually for ERC-20

* RustFmt example

* Move `Mapping` from `collections` to `lazy`

* Remove extra `0` in docs

Co-authored-by: Robin Freyler <robin.freyler@gmail.com>
@forgetso
Copy link
Contributor

Is there a plan for a function like get_mut as part of Mapping? How do we update a key value pair - just insert again?

@xgreenx
Copy link
Collaborator

xgreenx commented Nov 18, 2021

Is there a plan for a function like get_mut as part of Mapping? How do we update a key value pair - just insert again?

Yes, insert again=)

@Robbepop
Copy link
Collaborator Author

Is there a plan for a function like get_mut as part of Mapping? How do we update a key value pair - just insert again?

A get_mut function won't be part of the simple Mapping type but it will be available on types that build upon the Mapping type internally and provide a more high-level API.

@forgetso
Copy link
Contributor

forgetso commented Nov 19, 2021

Thanks for the info. Is iter deliberately not implemented for Mapping? I started converting my contract to Mapping but I have methods that previously used hashmap.keys(). Now I can't seem to create an iterator for Mapping to implement a keys style function.

    impl ProviderMapping {
        fn keys(&self) -> Vec<AccountId> {
            let mut keys: Vec<AccountId> = Vec::new();
            for (key, value) in &self.providers.iter() {
                keys.push(key);
            }
            keys
        }
for (key, value) in &self.providers.iter() {
   |                                                 ^^^^ method not found in `Mapping<ink_env::AccountId, Provider>`

Do I need to implement iter for my custom Mapping struct or should I wait for the higher level API that will build on Mapping?

@Robbepop
Copy link
Collaborator Author

If you come from the Solidity world then Mapping is literally just what Solidity provides to you with its own builtin mapping types which are ... just mappings and no real collections that do have knowledge about their contained elements.
So it is not the job of the Mapping type to provide collection APIs such as iter. This is a very common misunderstanding unfortunately.
If what you need is a collection then you can either build one yourself using the Mapping type internally OR use ink!'s ink_storage::HashMap.

@forgetso
Copy link
Contributor

forgetso commented Nov 19, 2021

Cool this is my first time coding smart contracts and I'm coming from a python/JS background, never written in Rust/C. I'll try creating an iterable Mapping as per this example and compare contract size to Hashmap. Thanks for the clarification, you probably saved me a few more hours or days of searching.

@xgreenx
Copy link
Collaborator

xgreenx commented Nov 19, 2021

Cool this is my first time coding smart contracts and I'm coming from a python/JS background, never written in Rust/C. I'll try creating an iterable Mapping as per this example and compare contract size to Hashmap. Thanks for the clarification, you probably saved me a few more hours or days of searching.

Better to avoid iteration through the map in the smart contracts because it can contain a lot of entries. If you need to resolve some local issue(for example, in your algorithm, you need properties of the map to solve some issues. But it is needed only during function execution and after that, you don't need to store that map into storage), you can use ink_prelude::collections::HashMap. Or if you know that count of entries is not big, then you can store ink_prelude::collections::HashMap into the storage(when you will load/save that map you will load/save the whole map with all entries).

@forgetso
Copy link
Contributor

forgetso commented Nov 24, 2021

I have reworked my contract using Mapping instead of Hashmap, which has reduced the size from 59.8K to 45K when building on Supercolony's reworked-common-part-of-message branch. However, I also removed any get functions and replaced them with Vectors that contain known keys for Mappings - so that will have helped.

One of the issues I ran into during this process is that enum is not supported by SpreadAllocate, which seems to be required as part of the new contract instantiation process.

I had to modify ink to allow enums to implement SpreadAllocate. Hopefully this will be allowed in ink in future.

I also tried and failed to generalise my types that build on Mapping, as mentioned here.

@xgreenx
Copy link
Collaborator

xgreenx commented Nov 29, 2021

I have reworked my contract using Mapping instead of Hashmap, which has reduced the size from 59.8K to 45K when building on Supercolony's reworked-common-part-of-message branch. However, I also removed any get functions and replaced them with Vectors that contain known keys for Mappings - so that will have helped.

One of the issues I ran into during this process is that enum is not supported by SpreadAllocate, which seems to be required as part of the new contract instantiation process.

I had to modify ink to allow enums to implement SpreadAllocate. Hopefully this will be allowed in ink in future.

I also tried and failed to generalise my types that build on Mapping, as mentioned here.

I tried your contract with a new version and it takes Original wasm size: 81.7K, Optimized: 32.4K(32355).

xgreenx pushed a commit to Supercolony-net/ink that referenced this pull request Feb 8, 2022
* Add `Mapping` storage collection

* Implement `insert` and `get` for `Mapping`

* Implement `SpreadLayout` for `Mapping`

* Fix typo

* Add some basic tests

* Fix some documentation formatting

* Use `PackedLayout` as trait bound instead of `Encode/Decode`

* Avoid using low level `ink_env` functions when interacting with storage

* RustFmt

* Appease Clippy

* Only use single `PhantomData` field

* Change `get` API to take reference to `key`

* Implement `TypeInfo` and `StorageLayout` for `Mapping`

* Properly gate `TypeInfo` and `StorageLayout` impls behind `std`

* Replace `HashMap` with `Mapping` in ERC-20 example

* Return `Option` from `Mapping::get`

* Update ERC-20 to handle `Option` returns

* Change `get` and `key` to use `Borrow`-ed values

* Add `Debug` and `Default` implementations

* Proper spelling

* Change `insert` to only accept borrowed K,V pairs

* Update ERC-20 example accordingly

* Make more explicit what each `key` is referring to

* Try using a `RefCell` instead of passing `Key` around

* Try using `UnsafeCell` instead

* Revert "Try using a `RefCell` instead of passing `Key` around"

This reverts commit cede033.

Using `RefCell`/`UnsafeCell` doesn't reduce the contract size more than
what we have now, and it introduced `unsafe` code. We believe the
limiting factor here is the `Key` type definition anyways.

* Clean up some of the documentation

* Simple Mapping type improvements (use-ink#979)

* Add `Mapping` storage collection

* Implement `insert` and `get` for `Mapping`

* Implement `SpreadLayout` for `Mapping`

* Fix typo

* Add some basic tests

* Fix some documentation formatting

* Use `PackedLayout` as trait bound instead of `Encode/Decode`

* Avoid using low level `ink_env` functions when interacting with storage

* RustFmt

* Appease Clippy

* Only use single `PhantomData` field

* Change `get` API to take reference to `key`

* Implement `TypeInfo` and `StorageLayout` for `Mapping`

* Properly gate `TypeInfo` and `StorageLayout` impls behind `std`

* Replace `HashMap` with `Mapping` in ERC-20 example

* Return `Option` from `Mapping::get`

* Update ERC-20 to handle `Option` returns

* Change `get` and `key` to use `Borrow`-ed values

* Add `Debug` and `Default` implementations

* Proper spelling

* Change `insert` to only accept borrowed K,V pairs

* Update ERC-20 example accordingly

* Make more explicit what each `key` is referring to

* Try using a `RefCell` instead of passing `Key` around

* Try using `UnsafeCell` instead

* Revert "Try using a `RefCell` instead of passing `Key` around"

This reverts commit cede033.

Using `RefCell`/`UnsafeCell` doesn't reduce the contract size more than
what we have now, and it introduced `unsafe` code. We believe the
limiting factor here is the `Key` type definition anyways.

* Clean up some of the documentation

* adjust the Mapping type for the new SpreadAllocate trait

* adjust ERC-20 example for changes in Mapping type

* remove commented out code

* add doc comment to new_init

* make it possible to use references in more cases with Mapping

* use references in more cases for ERC-20 example contract

* remove unnecessary references in Mapping methods

* refactor/improve pull_packed_root_opt utility method slightly

* fix ERC-20 example contract

The problem with *self.total_supply is that it may implicitly read from storage in case it has not yet read a value from storage whereas Lazy::set just writes the value to the Lazy instance.

Co-authored-by: Hernando Castano <hernando@hcastano.com>
Co-authored-by: Hernando Castano <HCastano@users.noreply.github.com>

* Use new `initialize_contract()` function

* Derive `SpreadAllocate` for `ink(storage)` structs

* Stop manually implementing SpreadAllocate for ERC-20

* Stop implementing `SpreadAllocate` in the storage codegen

* Derive `SpreadAllocate` manually for ERC-20

* RustFmt example

* Move `Mapping` from `collections` to `lazy`

* Remove extra `0` in docs

Co-authored-by: Robin Freyler <robin.freyler@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants