Hello! Welcome back to our module on Auditing and Optimizing Solana Programs.
In our last session, we established the foundational skill of performance tuning: measurement. You learned how to use transaction logs and the compute_fn! macro to precisely quantify the Compute Unit (CU) consumption of your program's instructions.
Today, we move from "how much" to "what and why." Armed with the ability to measure, we can now hunt for the common culprits of high compute usage. Your learning outcome for this lesson is to identify and refactor high-cost operations, such as deserialization and excessive logging. We'll examine these performance bottlenecks and learn practical techniques to mitigate them, transforming your programs from merely functional to highly efficient.
The Optimization Mindset: Measure, Identify, Refactor
Before we dive into specific techniques, it's important to adopt the right mindset. Optimization is not about guessing; it's a systematic process. The skills you learned in the previous lesson are your first and most important step.
- Measure: Get a baseline CU cost for your instructions.
- Identify: Use granular measurement to pinpoint the most expensive operations within an instruction.
- Refactor: Apply a specific optimization technique to the identified bottleneck.
- Measure Again: Verify that your change had the desired effect and didn't introduce any regressions.
This cycle is likely familiar from your front-end development experience, where you'd use browser profilers to find slow JavaScript functions or identify components causing unnecessary re-renders before refactoring them. The tools are different, but the principle is identical.
The image below from Helius illustrates the path you can take, moving down the stack from high-level convenience to low-level performance. Today, we'll focus on the first big step: moving from standard Anchor to more optimized patterns like zero_copy.

1. The Hidden Cost of Logging
Logging is an indispensable tool for debugging. However, on-chain logging is a privileged operation that writes data to the Solana runtime, and it can be surprisingly expensive. Excessive or inefficient logging is often the lowest-hanging fruit for optimization.
The key issue is not just the act of logging itself, but what you log and how you format it. Operations like string formatting and, especially, encoding binary data (like a Pubkey) into a human-readable format (Base58) are very CU-intensive.
How to Optimize Compute Usage on Solana
The official Solana developer guide, 'How to Optimize Compute Usage on Solana', provides clear examples of how expensive logging can be and shows a much more efficient alternative.
Please read the subsection titled 'Logging'. Pay close attention to the CU cost difference between logging a pubkey using msg! with string formatting versus using the built-in .log() method.
As you saw, the difference is dramatic:
- Inefficient (11,962 CU):
msg!("A string {}", ctx.accounts.counter.to_account_info().key());- This performs an expensive on-the-fly Base58 encoding of the public key and then concatenates it into a new string before logging.
- Efficient (262 CU):
ctx.accounts.counter.to_account_info().key().log();- This uses a highly optimized, built-in runtime function to log the pubkey directly, avoiding the expensive formatting overhead.
Refactoring Strategy:
- Audit your
msg!calls: Remove any logs that aren't critical for on-chain program logic. Use them for development, but consider removing them for production deployment. - Use efficient logging methods: When you need to log a
Pubkey, always prefer the.log()method overmsg!. - Avoid complex formatting: Minimize string concatenation and formatting within
msg!macros.
2. The Serialization/Deserialization Bottleneck
This is one of the most significant sources of compute usage in many Solana programs. When an instruction is called, the Solana runtime passes your program a raw byte buffer (&[u8]) for each account. To work with this data in a structured way (e.g., in a struct), it must be deserialized. When you're done, the modified data must be serialized back into the byte buffer to be saved.
By default, Anchor uses the Borsh serialization format. Borsh is robust and safe, but it involves a crucial step: it reads the account's byte buffer and copies it into a newly allocated region of memory to create your struct. When your instruction finishes, it serializes the struct back and copies it into the account's data buffer.
This copying process is the source of the high compute cost. The larger and more complex your account struct, the more expensive this process becomes.
Refactoring with Zero-Copy Deserialization
The most effective way to combat this is with zero-copy deserialization. The name says it all: it avoids the data copy. Instead of creating a new struct in new memory, it allows you to work with the data directly in the account's original memory buffer.
Let's see how to implement this.
The article 'Optimizing Solana Programs' from Helius provides a fantastic side-by-side comparison of a standard Anchor program and one refactored to use zero-copy.
Please read the section 'Zero-Copy Deserialization'. Focus on the 'Key Changes' subsection, which details the four main modifications required: Using AccountLoader instead of Account. Adding the #[account(zero_copy)] attribute. Using .load_init()? and .load_mut()? to access the data. Notice how the core logic (counter.count += 1;) remains the same, but the surrounding boilerplate changes.
Let's summarize the refactoring steps:
| Standard Anchor (Borsh) | Refactored (Zero-Copy) |
|---|---|
pub counter: Account<'info, Counter> | pub counter: AccountLoader<'info, CounterData> |
#[account] | #[account(zero_copy)] |
let counter = &mut ctx.accounts.counter; | let mut counter = ctx.accounts.counter.load_mut()?; |
| No specific memory layout required. | Struct must be #[repr(C)] or #[repr(packed)] for a defined layout. #[account(zero_copy)] handles this for you. Vec<T> and String are not allowed. |
While the CU savings in the simple counter example are modest, the Helius article correctly notes that for large, complex accounts or high-frequency instructions, zero-copy can cut your compute usage dramatically. It also offers a security benefit by preventing inconsistencies that can arise if the same (Borsh-deserialized) account is passed to a transaction multiple times.
Test your understanding!
You have the following standard Anchor account struct for a user profile:
#[account]
pub struct UserProfile {
pub authority: Pubkey,
pub name: [u8; 32],
pub level: u16,
pub xp: u64,
}
#[derive(Accounts)]
pub struct UpdateUserProfile<'info> {
#[account(mut, has_one = authority)]
pub profile: Account<'info, UserProfile>,
pub authority: Signer<'info>,
}
How would you refactor UserProfile and UpdateUserProfile to use zero-copy? What would the first line inside your instruction handler look like to get mutable access to the profile data?
Show answer
Here is the refactored code:
// 1. Add the `zero_copy` attribute and ideally rename the struct
// to indicate it's raw data (e.g., UserProfileData).
#[account(zero_copy)]
#[repr(C)] // Good practice to be explicit, though zero_copy implies it
pub struct UserProfile {
pub authority: Pubkey,
pub name: [u8; 32],
pub level: u16,
pub xp: u64,
}
#[derive(Accounts)]
pub struct UpdateUserProfile<'info> {
// 2. Use AccountLoader instead of Account.
#[account(mut, has_one = authority)]
pub profile: AccountLoader<'info, UserProfile>,
pub authority: Signer<'info>,
}
And the first line inside your instruction handler to access the data would be:
// 3. Use .load_mut()? to get a mutable reference.
let mut profile = ctx.accounts.profile.load_mut()?;
profile.xp += 10; // Now you can modify it directly.
3. Other Common High-Cost Operations
While logging and deserialization are major culprits, other patterns can also silently consume your CU budget. Being aware of them is the first step to spotting them in your code.
The Solana developer guide you read earlier highlights two more important ones.
How to Optimize Compute Usage on Solana
Let's revisit the Solana guide to look at two more quick but important optimization points: data types and PDA derivation.
Please read the subsections 'Data Types' and 'Program Derived Addresses'. For 'Data Types', note the CU difference between operations on a Vec<u64> versus a Vec<u8>. For 'Program Derived Addresses', focus on understanding why storing the bump seed after initialization is far more efficient than calling find_program_address every time.
Here's a quick summary of the refactoring strategies for these points:
-
Data Types:
- Identify: Look for fields in your account structs or variables in your logic that use large integer types (
u64,u128) when a smaller one (u8,u16,u32) would suffice. For example, if a value will never exceed 255, use au8, not au64. - Refactor: Change the type declaration. Every arithmetic operation on the smaller type will now be cheaper. These small savings add up quickly in complex calculations or loops.
- Identify: Look for fields in your account structs or variables in your logic that use large integer types (
-
PDA Derivation:
- Identify: Are you calling
Pubkey::find_program_address()in every instruction that needs to access a PDA? - Refactor: The
find_program_addressfunction can be expensive because it may need to iterate many times to find a valid bump seed. The optimal approach is:- Call
find_program_address()only once, when you initialize the PDA account. - Store the resulting
bumpseed as a field within the PDA account's data itself (e.g.,pub bump: u8). - In all subsequent instructions, use this stored
bumpin your Anchor constraints (seeds = [b"my_seed"], bump = my_pda_account.bump). This allows Anchor to reconstruct the address directly via the much cheaperPubkey::create_program_address(), bypassing the expensive search.
- Call
- Identify: Are you calling
Conclusion
In this lesson, we transitioned from measuring CU to actively hunting down and fixing performance hotspots. You now have a concrete list of the most common high-cost operations and, more importantly, a set of refactoring strategies to address them.
Key Takeaways:
- Excessive Logging: A common performance drain. Refactor by removing non-essential logs and using efficient methods like
.log()for pubkeys instead ofmsg!. - Borsh Deserialization: The default in Anchor is safe but costly due to data copying. The primary refactoring technique is to use zero-copy deserialization with
AccountLoaderand#[account(zero_copy)]. - Inefficient Data Types: Using oversized variables (e.g.,
u64for a small number) adds unnecessary computational overhead. Right-size your data. - Repetitive PDA Derivation: Calling
find_program_addressrepeatedly is wasteful. Store thebumpseed in the PDA account upon creation and reuse it.
Preview of the Next Lesson
We've focused on identifying and refactoring specific high-cost operations. In the next lesson, we will broaden our scope to optimize a program's compute usage by refining data structures and serialization methods. We'll look at how the layout of your accounts impacts performance and explore more advanced serialization techniques for when you need to squeeze out every last drop of performance.
Can't find a good explanation? Sign up and we'll make it for you
Sign up