CosmWasm / cosmwasm

Framework for building smart contracts in Wasm for the Cosmos SDK
https://www.cosmwasm.com/
Apache License 2.0
1.06k stars 328 forks source link

Negative affects of performance with execution by calling HostFunctions #1695

Open KamiD opened 1 year ago

KamiD commented 1 year ago

Background

Hi cosmwasmers~ I got an issue regarding the execution performance. We have migrated cosmwasm into OKTChain and OKBChain and now doing some requisite tests. while the pressure testing with contract interact, we found out that performance are limited by calling host-function. this is a huge performance gap between WASM and EVM while calling host-function. here is the performance analysis

Phenomenon

image

The issue purpose

We are highly requesting that your team members who could fix this issue asap as the performance is really important for the chains especially the WASM chain @ethanfrey @webmaster128 Thank you very much and our team will keep touching on it ~ Sincerely Yours

webmaster128 commented 1 year ago

Thank you for the detailed report. I have a few question before investigating further:

KamiD commented 1 year ago

pub fn execute( deps: DepsMut, _env: Env, _info: MessageInfo, msg: ExecuteMsg, ) -> Result<Response, ContractError> { ... let mut ret = 0; for i in 1..times { ret += TEST_VALUE.load(deps.storage).unwrap(); } avoidJump = ret; ... }



* No worries for 3, it's caused by the computer performance, I have tried on another PC and whole time costs just less than 1600ms
KamiD commented 1 year ago

@webmaster128 Hi, just want to know, is there any updates regard this issue?

KamiD commented 1 year ago

Here is my plan to improve the execution performance:

  1. Add new IO Function get_ex such as(to instead of function get):

    fn get_ex(&self, key: &[u8]) -> Option<Vec<u8>> {
        let mut vTemp:[u8;24] = [0;24];
        let key = build_region(key);
        let mut value = build_region(vTemp.as_ref());
        let key_ptr = &*key as *const Region as u32;
        let val_ptr = &*value as *const Region as u32;
    
        let read = unsafe { db_read_ex(key_ptr,val_ptr) }; // the keypoint is passing val_ptr to host function to avoid call allocate function to alloc memories.
        if read == 0 {
            // key does not exist in external storage
            return None;
        }
    
        let value_ptr = read as *mut Region;
        if read == val_ptr {
            //if value size is less than 24(roughly), return the value directly
            return Some(vTemp.to_vec());
        }
    
        let data = unsafe { consume_region(value_ptr) };
        Some(data)
    }
  2. Add host fonction do_db_read_ex and receive the extra param of output such as

    pub fn do_db_read_ex<A: BackendApi, S: Storage, Q: Querier>(
    env: &Environment<A, S, Q>,
    key_ptr: u32,
    value_ptr:u32,
    ) -> VmResult<u32> {
    let key = read_region(&env.memory(), key_ptr, MAX_LENGTH_DB_KEY)?;
    
    let (result, gas_info) = env.with_storage_from_context::<_, _>(|store| Ok(store.get(&key)))?;
    process_gas_info::<A, S, Q>(env, gas_info)?;
    let value = result?;
    
    let out_data = match value {
        Some(data) => data,
        None => return Ok(0),
    };
    write_to_contract_ex::<A, S, Q>(env, &out_data,value_ptr)
    }
  3. Especially for function : write_to_contract_ex:

    /// Creates a Region in the contract, writes the given data to it and returns the memory location
    fn write_to_contract_ex<A: BackendApi, S: Storage, Q: Querier>(
    env: &Environment<A, S, Q>,
    input: &[u8],
    output:u32,
    ) -> VmResult<u32> {
    // Just write result to output memory directly
    let ret = write_region(&env.memory(), output, input);
    
    return match ret {
        Ok(t) => {
            Ok(output)
        }
        _ => {// call original write function if failed to write_region to output directly
            write_to_contract(env, input)
        }
    };
    }
KamiD commented 1 year ago

@webmaster128 Pls help to check is this solution suitable for cosmwasm ?

webmaster128 commented 1 year ago

That's interesting work here. It would be heavily breaking and hardly make it to main CosmWasm in the near future. But still worth exploring.

Some thoughts:

KamiD commented 1 year ago
ethanfrey commented 1 year ago

In practice, we often see times between 40 microseconds to 100 microseconds when reading one value from the backing database.

1000000 read operations in one benchmark test

If your benchmark we run in a real blockchain, the IAVL overhead would likely be 40-100s (fast CPU, SSD, and moderate state size). This section you are looking to optimize takes around 2s for those 1 million operations.

My quick judgement is that it is not worth spending time optimizing this section until there is a 20-100x or more speedup in the data storage layer, and this section is a significant overhead for execution.

Furthermore, if a contract made 1 million read operations, it would likely use around 1-2 billion gas, which would never fit inside a blockchain transaction.

If that number were reduced to 100 or even 1000 read operations, then other fixed costs (like creating an Instance) would likely use a significantly more portion of the time. It is important to find the proper spots to optimize that will have impact in real world usecases.

While we could look into changes here, I think there are other areas that would be much more productive, like the IAVL tree in the SDK.

KamiD commented 1 year ago

Glad to got your responds and let me share the key points for this optimization. The influences of the changes are not only regarding the IO operations but also other calls of Host functions. As I know, all of the host functions which were registered by cosmwasm are using the same way to passing the params and calculate gas such as write_to_contract, read_region, process_gas_info Now we are focusing on optimizing those functions and got about 35% speedup of execution performance. I think it's worth to optimize. and we are keep digging on that~