anchor icon indicating copy to clipboard operation
anchor copied to clipboard

Use closure for init constraints

Open CanardMandarin opened this issue 10 months ago • 12 comments

Hello,

This PR is somewhat related to issues #2915 and #2920. It seems that with Anchor 0.29, stack usage in try_accounts increased and more easily causes Stack offset of X exceeded max offset of 4096 by X bytes, please minimize large stack variables error.

Existing projects using Anchor could have trouble migrating, and it looks like this question appears a lot in Discord. Using closures for init constraints helps decrease the stack usage and helps mitigate the issue.

What do you think?

CanardMandarin avatar Apr 28 '24 21:04 CanardMandarin

@CanardMandarin is attempting to deploy a commit to the coral-xyz Team on Vercel.

A member of the Team first needs to authorize it.

vercel[bot] avatar Apr 28 '24 21:04 vercel[bot]

@acheroncrypto @CanardMandarin , are closures always guranteed to be #[inline(never)] ? since that is what we want to achieve any solution that moves all this to a seperate stack frame. this solution is fine as long as the above gurantee is meant to hold.

nabeel99 avatar May 01 '24 15:05 nabeel99

so i tested it out with the following struct , reduced stack usage from 15k to 4k.

#[derive(Accounts)]
pub struct UpdateFooSecond<'info> {
    #[account(
        mut,
        constraint = &foo.load()?.get_second_authority() == second_authority.key,
    )]
    foo: AccountLoader<'info, Foo>,
    second_authority: Signer<'info>,
}

#[derive(Accounts)]
pub struct CreateBar<'info> {
    #[account(
        init,
        seeds = [authority.key().as_ref(), foo.key().as_ref()],
        bump,
        payer = authority, owner = ID,
        space = Bar::LEN + 8
    )]
    bar: AccountLoader<'info, Bar>,
    #[account(
        init,
        payer = authority, owner = ID,
        space = Bar::LEN + 8
    )]
    bar_3: AccountLoader<'info, Bar>,
    #[account(
        init,
        payer = authority, owner = ID,
        space = Bar::LEN + 8
    )]
    bar_4: AccountLoader<'info, Bar>,
    #[account(
        init,
        payer = authority, owner = ID,
        space = Bar::LEN + 8
    )]
    bar_5: AccountLoader<'info, Bar>,
    #[account(
        init,
        payer = authority, owner = ID,
        space = Bar::LEN + 8
    )]
    bar_6: AccountLoader<'info, Bar>,
    #[account(
        init,
        payer = authority, owner = ID,
        space = Bar::LEN + 8
    )]
    bar_7: AccountLoader<'info, Bar>,
    #[account(
        init,
        payer = authority, owner = ID,
        space = Bar::LEN + 8
    )]
    bar_8: AccountLoader<'info, Bar>,
    #[account(
        init,
        payer = authority, owner = ID,
        space = Bar::LEN + 8
    )]
    bar_9: AccountLoader<'info, Bar>,
    #[account(
        init,
        payer = authority, owner = ID,
        space = Bar::LEN + 8
    )]
    bar_10: AccountLoader<'info, Bar>,
    #[account(
        init,
        payer = authority, owner = ID,
        space = Bar::LEN + 8
    )]
    bar_11: AccountLoader<'info, Bar>,
    #[account(
        init,
        payer = authority, owner = ID,
        space = Bar::LEN + 8
    )]
    bar_12: AccountLoader<'info, Bar>,
    #[account(
        init,
        payer = authority, owner = ID,
        space = Bar::LEN + 8
    )]
    bar_13: AccountLoader<'info, Bar>,
    #[account(
        init,
        payer = authority, owner = ID,
        space = Bar::LEN + 8
    )]
    bar_14: AccountLoader<'info, Bar>,
    #[account(
        init,
        payer = authority, owner = ID,
        space = Bar::LEN + 8
    )]
    bar_15: AccountLoader<'info, Bar>,
    #[account(
        init,
        payer = authority, owner = ID,
        space = Bar::LEN + 8
    )]
    bar_16: AccountLoader<'info, Bar>,
    #[account(
        init,
        payer = authority, owner = ID,
        space = Bar::LEN + 8
    )]
    bar_17: AccountLoader<'info, Bar>,
    #[account(
        init,
        payer = authority, owner = ID,
        space = Bar::LEN + 8
    )]
    bar_18: AccountLoader<'info, Bar>,
    #[account(
        init,
        payer = authority, owner = ID,
        space = Bar::LEN + 8
    )]
    bar_19: AccountLoader<'info, Bar>,
    #[account(
        init,
        payer = authority, owner = ID,
        space = Bar::LEN + 8
    )]
    bar_20: AccountLoader<'info, Bar>,
    #[account(mut)]
    authority: Signer<'info>,
    foo: AccountLoader<'info, Foo>,
    system_program: AccountInfo<'info>,
}**
**

Got good results , without closure : Error: Function _ZN95_$LT$zero_copy..CreateBar$u20$as$u20$anchor_lang..Accounts$LT$zero_copy..CreateBarBumps$GT$$GT$12try_accounts17hc3d5369b2e707256E Stack offset of **15208** exceeded max offset of **4096** by **11112** bytes, please minimize large stack variables with closure : Error: Function _ZN95_$LT$zero_copy..CreateBar$u20$as$u20$anchor_lang..Accounts$LT$zero_copy..CreateBarBumps$GT$$GT$12try_accounts17ha6b9d25c9bc115a3E Stack offset of **4632** exceeded max offset of **4096** by **536** bytes, please minimize large stack variables For now it seems the closures are acting like #[inline(never)] , but idk if this behavior will stay the same across rust versions, one cant at this point in time use attributes on closures so we cant add a the inline attribute explicitly on the closures , tracking rust rfc : https://github.com/rust-lang/rust/issues/15701

nabeel99 avatar May 01 '24 22:05 nabeel99

For now it seems the closures are acting like #[inline(never)] , but idk if this behavior will stay the same across rust versions, one cant at this point in time use attributes on closures so we cant add a the inline attribute explicitly on the closures , tracking rust rfc : https://github.com/rust-lang/rust/issues/15701

hm? yeah you can.

workingjubilee avatar May 17 '24 02:05 workingjubilee

see https://github.com/rust-lang/rust/issues/15701#issuecomment-2073675764

workingjubilee avatar May 17 '24 02:05 workingjubilee

Hey

For now it seems the closures are acting like #[inline(never)] , but idk if this behavior will stay the same across rust versions, one cant at this point in time use attributes on closures so we cant add a the inline attribute explicitly on the closures , tracking rust rfc : rust-lang/rust#15701

hm? yeah you can.

see rust-lang/rust#15701 (comment)

Seems i misread that pr and thought its still not avbl on stable-wrapping in curly braces compiles fine- thanks for the headsup @workingjubilee .

nabeel99 avatar May 19 '24 17:05 nabeel99

You can always add the attribute

fn main () {
    let demo = {
        #[inline(never)] || println!("hello")
    };
    demo();
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=480eca029ed7e86974043c9b717a528e

cavemanloverboy avatar Jul 07 '24 16:07 cavemanloverboy

You can always add the attribute


fn main () {

    let demo = {

        #[inline(never)] || println!("hello")

    };

    demo();

}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=480eca029ed7e86974043c9b717a528e

This is exactly what i am doing, wrapped it in a inline closure,the only issue which is preventing me from making a pr is, how do we add bench test for this, is there a way to deterministically measure stack frame size usage, need this since i need to add before and after differences as part of the pr. See this

nabeel99 avatar Jul 07 '24 16:07 nabeel99

is there a way to deterministically measure stack frame size usage

You can check the generated assembly code for the contract and analyze the largest offset from R10 in load and store instructions.

LucasSte avatar Jul 08 '24 21:07 LucasSte

This is exactly what i am doing, wrapped it in a inline closure,the only issue which is preventing me from making a pr is, how do we add bench test for this, is there a way to deterministically measure stack frame size usage, need this since i need to add before and after differences as part of the pr.

Not really a good one, no, and Rust does not promise that we do not make the stack size bigger for essentially no reason. ( I mean, we'll probably have a reason, but you might think of it as a silly one, so you would see it as No Reason At All. )

workingjubilee avatar Jul 08 '24 22:07 workingjubilee

This is exactly what i am doing, wrapped it in a inline closure,the only issue which is preventing me from making a pr is, how do we add bench test for this, is there a way to deterministically measure stack frame size usage, need this since i need to add before and after differences as part of the pr.

Not really a good one, no, and Rust does not promise that we do not make the stack size bigger for essentially no reason. ( I mean, we'll probably have a reason, but you might think of it as a silly one, so you would see it as No Reason At All. )

Hey ser @workingjubilee could you elaborate on your point, did not exactly understand, also the approach for inline(never) closure is to force rust to call the providing body of the closure in a seperate function as the ebpf vm provides a fresh stack frame for each function so that is what we are trying to achieve via inline(never).

nabeel-uncx avatar Jul 09 '24 08:07 nabeel-uncx

inline(never) and inline(always) are both actually just hints, and the Rust compiler is free to disregard or overrule them when emitting code. See https://doc.rust-lang.org/reference/attributes/codegen.html

workingjubilee avatar Jul 12 '24 01:07 workingjubilee