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

lang: Add LazyAccount #3194

Merged
merged 4 commits into from
Sep 1, 2024

Conversation

acheroncrypto
Copy link
Collaborator

Problem

There are a few related problems:

  • Excessive stack usage in Anchor generated functions
  • Waste of compute units when deserializing unused account fields

Solution

Add a new account type, LazyAccount, that conveniently allows deserialization of individual fields on-demand.

When to use

This is currently an experimental account type and therefore should only be used when you're running into performance issues.

It's best to use LazyAccount when you only need to deserialize some of the fields, especially if the account is read-only.

Replacing Account (including Boxed) with LazyAccount can improve both stack memory and compute unit usage. However, this is not guaranteed. For example, if you need to deserialize the account fully, using LazyAccount will have additional overhead and therefore use slightly more compute units.

Currently, using the mut constraint eventually results in the whole account getting deserialized, meaning it won't use fewer compute units compared to Account. This might get optimized in the future.

Features

  • Can be used as a replacement for Account.
  • Checks the account owner and its discriminator.
  • Does not check the type layout matches the defined layout.
  • All account data can be deserialized with load and load_mut methods. These methods are non-inlined, meaning that they're less likely to cause stack violation errors.
  • Each individual field can be deserialized with the generated load_<field> and load_mut_<field> methods.

Example

use anchor_lang::prelude::*;

declare_id!("LazyAccount11111111111111111111111111111111");

#[program]
pub mod lazy_account {
    use super::*;

    pub fn init(ctx: Context<Init>) -> Result<()> {
        let mut my_account = ctx.accounts.my_account.load_mut()?;
        my_account.authority = ctx.accounts.authority.key();

        // Fill the dynamic data
        for _ in 0..MAX_DATA_LEN {
            my_account.dynamic.push(ctx.accounts.authority.key());
        }

        Ok(())
    }

    pub fn read(ctx: Context<Read>) -> Result<()> {
        // Cached load due to the `has_one` constraint
        let authority = ctx.accounts.my_account.load_authority()?;
        msg!("Authority: {}", authority);
        Ok(())
    }

    pub fn write(ctx: Context<Write>, new_authority: Pubkey) -> Result<()> {
        // Cached load due to the `has_one` constraint
        *ctx.accounts.my_account.load_mut_authority()? = new_authority;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Init<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,
    #[account(
        init,
        payer = authority,
        space = MyAccount::DISCRIMINATOR.len() + MyAccount::INIT_SPACE
    )]
    pub my_account: LazyAccount<'info, MyAccount>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Read<'info> {
    pub authority: Signer<'info>,
    #[account(has_one = authority)]
    pub my_account: LazyAccount<'info, MyAccount>,
}

#[derive(Accounts)]
pub struct Write<'info> {
    pub authority: Signer<'info>,
    #[account(mut, has_one = authority)]
    pub my_account: LazyAccount<'info, MyAccount>,
}

const MAX_DATA_LEN: usize = 256;

#[account]
#[derive(InitSpace)]
pub struct MyAccount {
    pub authority: Pubkey,
    pub fixed: [Pubkey; 8],
    // Dynamic sized data also works, unlike `AccountLoader`
    #[max_len(MAX_DATA_LEN)]
    pub dynamic: Vec<Pubkey>,
}

See the full example here.

Safety

The safety checks are done using the account's discriminator and the account's owner (similar to Account). However, you should be extra careful when deserializing individual fields if, for example, the account needs to be migrated. Make sure the previously serialized data always matches the account's type identically.

Performance

Memory

All fields (including the inner account type) are heap allocated. It only uses 24 bytes (3x pointer size) of stack memory in total.

It's worth noting that where the account is being deserialized matters. For example, the main place where Anchor programs are likely to hit stack violation errors is a generated function called try_accounts (you might be familiar with it from the mangled build logs). This is where the instruction is deserialized and constraints are run. Although having everything at the same place is convenient for using constraints, this also makes it very easy to use the fixed amount of stack space (4096 bytes) SVM allocates just by increasing the number of accounts the instruction has. In SVM, each function has its own stack frame, meaning that it's possible to deserialize more accounts simply by deserializing them inside other functions (rather than in try_accounts which is already quite heavy).

The mentioned stack limitation can be solved using dynamic stack frames, see SIMD-0166.

Compute units

Compute is harder to formulate, as it varies based on the inner account's type. That being said, there are a few things you can do to optimize compute units usage when using LazyAccount:

  • Order account fields from fixed-size data (e.g. u8, Pubkey) to dynamic data (e.g. Vec).
  • Order account fields based on how frequently the field is accessed (starting with the most frequent).
  • Reduce or limit dynamic fields.

Note: This is currently an experimental account, and is therefore behind a feature flag (lazy-account).

LazyAccount combined with #2939 results in a massive reduction in stack usage and thus resolves #3060.

Copy link

vercel bot commented Aug 24, 2024

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

A member of the Team first needs to authorize it.

@acheroncrypto acheroncrypto marked this pull request as ready for review September 1, 2024 01:03
@acheroncrypto acheroncrypto merged commit 879601e into coral-xyz:master Sep 1, 2024
50 of 51 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature lang performance Performance related issues/PRs
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Reduce stack usage of try_accounts
1 participant