#Introducing batched getters as a standard

21 messages · Page 1 of 1 (latest)

iron isle
#

Edit for new readers: there will be tx-level caching in next releases, first call to a contract parses the module and subsequent calls get the already parsed module, resulting in batched calls not being needed. Thus this is not a standard you'd want to enforce unless you're looking for something very specific.

The current metering makes it very expensive to make a new VM instantiation (i.e a cross contract call), which makes an invocation very expensive when it needs to call a contract multiple times. For example, as @wide jolt pointed out (#1113855389100945439 message) they'll need to add batched price retrievals in order not to make clients instantiate many VMs and incurr in higher gas prices or even getting their transaction reverted because of usage costs.

I think that the standard token interface should enforce a similar design for batch-retrieving balances:

fn batched_balance(e: Env, ids: Vec<Address>) -> Vec<i128> {
        let mut balances: Vec<i128> = Vec::new(&e);
        for id in ids.iter() {
            balances.push_back(read_balance(&e, id.unwrap()))
        }

        balances
    }

This makes invocations that require getting more than one balance much more efficient. The delta cpuIns cost between these two functions is 994383:

#[contractimpl]
impl ContractB {
    pub fn bal_unopt(env: Env, contract: Address, x: Address, y: Address) {
        let client = contract_a::Client::new(&env, &contract);
        client.balance(&x);
        client.balance(&y);
    }

    pub fn bal_opt(env: Env, contract: Address, x: Address, y: Address) -> Vec<i128> {
        let client = contract_a::Client::new(&env, &contract);
        client.batched_balance(&vec![
            &env,
            x,
            y,
        ])
    }
}
#

When retrieving more balances, for example when 12 vm instantiations are required the delta between these other two functions is 11290021:

#[contractimpl]
impl ContractB {
    pub fn bal_unopt(env: Env, contract: Address, x: Address, y: Address) {
        let client = contract_a::Client::new(&env, &contract);
        client.balance(&x);
        client.balance(&y);
        client.balance(&x);
        client.balance(&y);
        client.balance(&x);
        client.balance(&y);
        client.balance(&x);
        client.balance(&y);
        client.balance(&x);
        client.balance(&y);
        client.balance(&x);
        client.balance(&y);
    }

    pub fn bal_opt(env: Env, contract: Address, x: Address, y: Address) -> Vec<i128> {
        let client = contract_a::Client::new(&env, &contract);
        client.batched_balance(&vec![
            &env,
            x.clone(),
            y.clone(),
            x.clone(),
            y.clone(),
            x.clone(),
            y.clone(),
            x.clone(),
            y.clone(),
            x.clone(),
            y.clone(),
            x.clone(),
            y.clone(),
        ])
    }
}

Which is a very significant additional cost, almost 1/3 (1/3.5) of the maximum cpuIns budget.

I don't like the idea of adding more functions to standard contracts, but this seems like an important optimization.

unreal meteor
#

the annoying part here is that this would need to be implemented in SAC, but OTOH this optimization is pretty much meaningless for SAC

iron isle
unreal meteor
#

because there are no VM calls involved

#

but that's probably not a big deal

iron isle
#

really? I thought that a SAC invocation would still instantiate a VM, so are SACs basically host functions?

unreal meteor
#

yeah, they're 'built-in' contracts

#

they're executed in host

#

batched calls would still be cheaper probably, but only marginally

iron isle
# unreal meteor they're executed in host

didn't know they were executed in the host. Anyways, probably worth introducing this optimization in soroban-only token contracts then (not sure though if these will be supported by most wallets)

modest fern
#

I think we could make the cost model reflect "first call pays for parsing, subsequent calls (in the same transaction) get a pre-parsed module". that's realistic and doesn't make costs depend on transaction-set compostion.

#

i.e. we could model "cache hit, within transaction" reasonably easily, even if we don't have caching in place yet, we're certainly intending to put it in and a reasonable first step is caching within a transaction / host. I wasn't sure this was a scenario (contract A calls contract B multiple times in a single tx) that was likely to happen, but it sounds like it is?

iron isle
#

Looks like it would solve also this issue and would be doable since it would only be tx-level, not tx-set-level

#

Subsequent cross contract calls are definitely a good example that makes in-tx caching really useful, also as I imagine it would incetivize developers to modularize their functions more and remove unneded "wrapper" functions without incurring in costs that are much higher. i.e contract function a and contract function b need/can under certain cirumstances to be called within a single transaction, so developer implements also function c that "wraps" a() and b(), so does both (as they worry about a and b instantiating two VMs).

tight horizon
tight horizon
#

@iron isle given that there's a path forward that doesn't require contracts implementing batch functionality I suggest that we don't encourage this, especially not for standard interfaces

iron isle
#

Added a disclaimer on top of the post

iron isle
#

@modest fern had a doubt about the implementation you talked about

first call pays for parsing, subsequent calls (in the same transaction) get a pre-parsed module

Is parsing the the binary into a module what's actually expensive in this situation?

Both validation checks and vm instantiation would still happen before the invocation even with a pre-parsed module if I'm not wrong (?)

Or you mean that the subsequent calls get the module instance with all its context?