-
Notifications
You must be signed in to change notification settings - Fork 432
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
Reworked message dispatch logic #1017
Reworked message dispatch logic #1017
Conversation
let input_bindings = generator::input_bindings(constructor.inputs()); | ||
let input_bindings = constructor.inputs() | ||
.map(|_| quote! { | ||
::scale::Decode::decode(&mut _input).map_err(|_| ::ink_lang::reflect::DispatchError::CouldNotReadInput)? |
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.
Also removing of .map_err
and usage of .unwrap()
reduce size for ~500 bytes for each contract in the description. So we can create two analogs of CALLABLE
(for debug
and release
mode).
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.
Erc20
: Original wasm size: 31.5K, Optimized: 9.1K(9067) - saved 571
Erc1155
: Original wasm size: 83.9K, Optimized: 46.2K(46168) - saved 600
multisig
: Original wasm size: 103.8K, Optimized: 46.8K(46798) - saved 359
@xgreenx You can get the |
|
Oh, after my change the contract requires 53k additional gas=D Because I'm allocating 16MB in stack=D But I think we can decide in this PR how to optimize it(for example use static buffer form instance) |
🦑 📈 ink! Example Contracts ‒ Changes Report 📉 🦑These are the results when building the
Link to the run | Last update: Thu Jan 27 10:40:02 CET 2022 |
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.
I have not yet reviewed the whole thing but I am very skeptical of the introduction of the local static buffer. Imo we should make sure to use the already existing global static buffer in those cases.
pub trait ContractMessageDecoder { | ||
/// The ink! smart contract message decoder type. | ||
type Type: scale::Decode + ExecuteDispatchable; | ||
pub trait ContractMessageExecutor { |
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 remove all the documentation and usage examples? Please re-add or write new.
pub trait ContractConstructorDecoder { | ||
/// The ink! smart contract constructor decoder type. | ||
type Type: DecodeDispatch + ExecuteDispatchable; | ||
pub trait ContractConstructorExecutor { |
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 re-add or write new docs with usage examples.
); | ||
} | ||
} | ||
// use ink_lang as ink; |
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 re-enable this UI test if possible. We should always try to have testable code.
crates/env/src/api.rs
Outdated
/// # Note | ||
/// | ||
/// This function stops the execution of the contract immediately. | ||
pub fn return_value_scoped(return_flags: ReturnFlags, return_value: &[u8]) -> ! { |
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.
very confusing name imo. The scoped buffer concept is an internal low-level concept that should not be exposed to the user. I'd rename this to return_bytes
since that's what it is.
crates/env/src/api.rs
Outdated
/// # Note | ||
/// | ||
/// This function stops the execution of the contract immediately. | ||
pub fn input_scoped(buffer: &mut [u8]) { |
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.
Very confusing name for the reason already described in another comment. I would rename this to input_bytes
since that is what is actually is.
crates/env/src/backend.rs
Outdated
@@ -220,6 +222,17 @@ pub trait EnvBackend { | |||
where | |||
R: scale::Encode; | |||
|
|||
/// Returns the buffer to the caller of the executed contract. |
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.
/// Returns the buffer to the caller of the executed contract. | |
/// Returns the bytes to the caller of the executed contract. |
crates/env/src/backend.rs
Outdated
/// | ||
/// The `flags` parameter can be used to revert the state changes of the | ||
/// entire execution if necessary. | ||
fn return_value_scoped(&mut self, flags: ReturnFlags, return_value: &[u8]) -> !; |
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.
fn return_value_scoped(&mut self, flags: ReturnFlags, return_value: &[u8]) -> !; | |
fn return_value_scoped(&mut self, flags: ReturnFlags, returned_bytes: &[u8]) -> !; |
crates/env/src/api.rs
Outdated
/// # Note | ||
/// | ||
/// This function stops the execution of the contract immediately. | ||
pub fn return_value_scoped(return_flags: ReturnFlags, return_value: &[u8]) -> ! { |
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.
pub fn return_value_scoped(return_flags: ReturnFlags, return_value: &[u8]) -> ! { | |
pub fn return_bytes(return_flags: ReturnFlags, returned_bytes: &[u8]) -> ! { |
crates/env/src/api.rs
Outdated
/// # Note | ||
/// | ||
/// This function stops the execution of the contract immediately. | ||
pub fn input_scoped(buffer: &mut [u8]) { |
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.
pub fn input_scoped(buffer: &mut [u8]) { | |
pub fn input_bytes(buffer: &mut [u8]) -> Result<()> { |
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.
Another problem I have with this API is that it does not cover the erraneous case where buffer
is too small to hold the entire given input.
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.
But we used the same schema in the previous implementation. We decoded the whole ink's message variant
const CAPACITY: usize = 1 << 14; | ||
let mut local_buffer: [u8; CAPACITY] = [0; CAPACITY]; |
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 use the already existing static buffer that each ink! smart contract already has? I do not see a technical reason why using the already existing static buffer should be less efficient than using a local 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.
We should use a static buffer because it allows us to not allocate the memory in runtime -> less gas.
I will try to rework it to use a static buffer. The problem is that off_chain
and off_chain_experemental
don't use a static buffer. So I can't return &[u8]
. But if you are okey with that idea I will add a static buffer to that engines.
The local buffer is used to fastly create that PR and check the impact=)
I want to clarify, that it is draft PR to show the idea=) I will add documentation and tests when you will agree with the idea(= |
.gitlab-ci.yml
Outdated
@@ -6,11 +6,7 @@ | |||
|
|||
|
|||
stages: | |||
- check |
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 does this PR change the CI?
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.
I will revert it back after merging with master
crates/env/src/api.rs
Outdated
pub fn input_bytes<'a>() -> &'a [u8] { | ||
<EnvInstance as OnInstance<'a>>::on_instance(|instance: &'a mut EnvInstance| { | ||
EnvBackend::input_bytes(instance) | ||
}) |
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 API as stated is extremely unsafe to use by design. This is exactly why the decode_input
API exists as is. Just notice how with this you could safely (!!) do the following:
let input = ink_env::input_bytes(); // Returns a slice refering to ink!'s static buffer
let caller = ink_env::set_contract_storage(key, &[0x1; 1]);
// The above line mutates ink!'s static buffer to hold an AccountId.
// Note that any other API that interferes with ink!'s static buffer could have been taken as an example here.
println!("{:?}", input);
// At this point the above input binding no longer holds the input but the AccountId.
I think I do not have to point out how extremely unsafe this API design is and therefore I would never accept a PR introducing it, even if it was marked unsafe since we do not want to introduce unsafe APIs if we can do better.
Basically with ink!'s static buffer the rule number 1 is to never return a reference to it.
So far we have been successful in designing APIs that are efficient and work around this fact.
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.
The idea was that borrow checker will prevent you from the usage of ink_env::caller()
if you will use input
after that. But I tested it now and... It allows that=D So yes, it should be changed.
Do you have idea how better to provide the usage of static buffer to the user in safety way?
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.
Do you have idea how better to provide the usage of static buffer to the user in safety way?
Well I designed the decode_input
API exactly this way because of exactly this issue. So my proposal is to keep decode_input
API as is.
Even with a closure approach such as
fn input_bytes<'a, F, T>(f: F) -> Result<T, Error>
where
F: FnOnce(&'a [u8]) -> T;
This cannot work since then you encounter the same problem within the closure body.
Anything else would require expensive runtime checks such as with RefCell
.
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.
But the idea of the change is to decode directly in the place where we want to use arguments. Instead of decoding them in another place and then moving them to the place where we want to use them=)
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.
I think a better idea is to still use decode_input
but not move the arguments and instead use references where the user wants to use references. Let me refer to this issue for this use case.
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.
I meant that we can change code-generation and optimize it do one ::ink_storage::traits::push_spread_root at the end of execute_dispatchable instead of the end of each message section.
Yes, there is a lot of optimization potential in the way we currently dispatch ink! constructors and messages but none of which requires changes in the ink_env
APIs imo. That's what I meant.
BTW, It should have the same performance. But in the case of a local buffer you need to allocate that memory in the stack - it is additional operations in Runtime.
Yes, so theoretically a static buffer should be even more efficient, right? ;)
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.
Adding artificial methods to ink! trait definitions is a no-go. We must make sure that ink! modifies trait definitions (and others) as minimally as possible.
I agree with that. But I think that this is the only possible solution at the moment if we want to support all the features of traits in rust. Because if the user defines type alias, or constant, or super trait, you will not able to do a trick as you did for __ink_TraitInfo
=)
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.
I think we should not discuss all at once. Please let us focus on the improvements to the dispatch logic in this PR.
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.
I mentioned trait because it requires the next logic:
First, we need to get 4 bytes to find a selector. After we know which method we need to call. After that method will decode the remaining bytes into arguments.
So it is why I'm trying to decode them agnoticly=)
What do you think about two static buffers? One will be always read-only and will contain input bytes. Another will be used for encoding/decoding. In this case, the user will have the access to the input at any time. And he can decode what he wants by himself.
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 always get the whole input for the user(even if he wants only 4 bytes we will read all 16 KB). And if he wants to have a simple signature of method(with 0 arguments) but later he wants to decode some parts of the input, he again should get the whole data. And it can be expensive to call ext::seal_input
each time. So I think the idea with a second array only for input is not so bad(=
d2b7a52
to
b2d86fa
Compare
Codecov Report
@@ Coverage Diff @@
## master #1017 +/- ##
==========================================
+ Coverage 75.69% 78.44% +2.74%
==========================================
Files 252 253 +1
Lines 9395 9487 +92
==========================================
+ Hits 7112 7442 +330
+ Misses 2283 2045 -238
📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more |
The change reduces the size of each contract except subcontracts of the The open question is: Are we okay to have a separate static buffer to store input of the contract? |
I found that all contracts from workspace contain debug information(stuff related to Removing |
The change is ready, so I'm waiting for your comments=) The change affects |
@xgreenx can you update this for the latest master? I'm curious as to how the size improvements look. Since this is a big change to the dispatch logic I think we should only move forward with it if the savings look good |
4551a13
to
55bbdae
Compare
|
Yep, looks like there's a problem (paritytech/ink-waterfall#20). The job did succeed though, and here are the results (link to run):
So the savings are alright, but they're not mindblowing either. I'm on the fence about this given the nature of the changes, but give me some time to try and better understand the new dispatch logic you're proposing before we decide what to do |
The more methods in the contract, the stronger the effect of the change. |
@xgreenx This has been neglected for a while (I guess mostly by us, sorry). If you want to revive it please open a new PR so we can a fresh perspective and set of sizes |
In this PR I moved common parts of each message to
execute_dispatchable
(pull of the contract). In the future, we can optimize it to do one push of the contract.Introduced an additional static buffer to store input. Now the user has access to the input buffer at any time during the execution of the contract(it is loaded only one time). This buffer is needed to rework the decoding process.
Moved the decode logic to the
CALLABLE
part. It allows the compiler to use value after decoding and do some optimization.Call
unwrap
instead of passing error upper duringexecute dispatchable
and decoding. It allows the compiler to optimize the code better.Optimized
return_value
function to simply use buffer for encoding the result. We don't need complex methods fromScoppedBuffer
because after the call of that method the program will exit.All these optimizations allow reducing the code of optimized contracts well.
Erc20
takes 7862 bytes instead of 10282(saved 2420).The more methods in the contract, the stronger the effect of the change.
That contract contains a lot of methods and the chagne saved 6489 bytes:
Original wasm size: 71.8K, Optimized: 38.8K(38844)
->Original wasm size: 81.7K, Optimized: 32.4K(32355)