返回 SC 笔记
SC Day 76

Solana 性能优化 — 账户数据打包/Compute Units/并行交易

### 1. Solana 性能模型概览

2026-06-25
第四阶段:综合实战 (73-80)
solanaperformancezero-copycompute-unitsparalleloptimization

日期: 2026-06-25 方向: Solana 阶段: 第四阶段:综合实战 (73-80) 标签: #solana #performance #zero-copy #compute-units #parallel #optimization


今日目标

类型内容
学习Solana 性能优化三大方向:数据打包、CU 优化、并行执行模型
实操将 Vault 程序用 zero-copy 重构,测量和对比 CU 消耗
产出优化前后对比数据 + Solana 性能优化检查清单

核心概念

1. Solana 性能模型概览

Solana 的性能限制与 EVM 完全不同:

维度EVM (Ethereum)Solana
计算限制Gas (30M per block)Compute Units (200K per instruction, 1.4M per tx)
存储成本SSTORE 极贵 (20K gas)Rent-exempt 一次性付费
并行执行无(顺序执行)可并行(Sealevel 引擎)
交易大小无硬限制1232 bytes
数据序列化ABI 编码Borsh / Zero-Copy
状态访问合约内部 mapping外部账户传入

2. 账户数据打包 — Zero-Copy 反序列化

标准 Borsh 反序列化的问题

Anchor 默认使用 Borsh 序列化/反序列化,每次访问账户数据时会将整个账户数据拷贝到内存

// 标准 Borsh 方式
#[account]
pub struct LargeAccount {
    pub authority: Pubkey,     // 32 bytes
    pub data: [u64; 1000],     // 8000 bytes
    pub metadata: [u8; 2000],  // 2000 bytes
}
// 总计约 10,032 bytes

// 每次访问都需要:
// 1. 从账户数据区拷贝 10,032 bytes 到堆内存
// 2. Borsh 反序列化
// 3. 操作完后再序列化写回
// 消耗大量 Compute Units!

Zero-Copy 解决方案

Zero-Copy 直接将账户数据内存映射为 Rust 结构体引用,无需拷贝:

use anchor_lang::prelude::*;

/// Zero-Copy 账户 - 直接操作底层数据,无需序列化/反序列化
/// 要求:
/// 1. 所有字段必须实现 Pod + Zeroable(bytemuck traits)
/// 2. 结构体必须是 #[repr(C)] 或 #[repr(packed)]
/// 3. 不能使用 String、Vec 等动态类型
#[account(zero_copy)]
#[repr(C)]
pub struct LargeOrderBook {
    pub authority: Pubkey,           // 32 bytes
    pub market_id: u64,              // 8 bytes
    pub order_count: u64,            // 8 bytes
    pub orders: [Order; 256],        // 256 * 48 = 12,288 bytes
    pub _padding: [u8; 4],           // 对齐填充
}

#[zero_copy]
#[repr(C)]
pub struct Order {
    pub owner: Pubkey,    // 32 bytes
    pub price: u64,       // 8 bytes
    pub amount: u64,      // 8 bytes
}
// 每个 Order = 48 bytes

// 使用 AccountLoader 代替 Account
#[derive(Accounts)]
pub struct PlaceOrder<'info> {
    // AccountLoader 使用 zero-copy 加载
    #[account(mut)]
    pub order_book: AccountLoader<'info, LargeOrderBook>,

    pub authority: Signer<'info>,
}

pub fn place_order(ctx: Context<PlaceOrder>, price: u64, amount: u64) -> Result<()> {
    // load_mut() 返回 RefMut,直接操作底层内存
    let order_book = &mut ctx.accounts.order_book.load_mut()?;

    let idx = order_book.order_count as usize;
    require!(idx < 256, ErrorCode::OrderBookFull);

    // 直接写入内存,无需序列化
    order_book.orders[idx] = Order {
        owner: ctx.accounts.authority.key(),
        price,
        amount,
    };
    order_book.order_count += 1;

    Ok(())
}

// 只读访问
pub fn get_order(ctx: Context<GetOrder>, index: u64) -> Result<()> {
    // load() 返回 Ref,只读引用
    let order_book = ctx.accounts.order_book.load()?;

    let order = &order_book.orders[index as usize];
    msg!("Order: price={}, amount={}", order.price, order.amount);

    Ok(())
}

CU 消耗对比

操作:写入一个 Order 到含 256 个 Order 的 OrderBook

标准 Borsh:
  - 反序列化整个 OrderBook: ~15,000 CU
  - 写入 Order:               ~200 CU
  - 序列化写回:              ~15,000 CU
  - 总计:                    ~30,200 CU

Zero-Copy:
  - load_mut():              ~500 CU
  - 写入 Order:              ~200 CU
  - 自动写回(内存映射):     ~0 CU
  - 总计:                     ~700 CU

节省: ~97% Compute Units!

Zero-Copy 限制

// ❌ 不能使用动态类型
#[account(zero_copy)]
pub struct Invalid {
    pub name: String,        // 错误!String 是动态大小
    pub items: Vec<u64>,     // 错误!Vec 是动态大小
    pub option: Option<u64>, // 错误!Option 的内存布局不确定
}

// ✅ 必须使用固定大小类型
#[account(zero_copy)]
#[repr(C)]
pub struct Valid {
    pub name: [u8; 32],      // 固定 32 字节字符串
    pub items: [u64; 100],   // 固定数组
    pub has_value: u8,       // 用 u8 代替 Option (0 = None, 1 = Some)
    pub value: u64,
}

3. Compute Units 优化

CU 预算管理

use anchor_lang::prelude::*;
use solana_program::compute_budget::ComputeBudgetInstruction;

// 每条指令默认 200,000 CU
// 每笔交易最多 1,400,000 CU

// 客户端可以请求更多 CU(需要额外费用)
// const modifyComputeUnits = ComputeBudgetProgram.setComputeUnitLimit({
//     units: 400000,
// });

// 常见操作的 CU 消耗
// SHA256 hash:           ~100 CU
// ED25519 verify:        ~2,000 CU
// Borsh deserialize:     ~数百到数万 CU(取决于数据大小)
// CPI (跨程序调用):       ~10,000+ CU 基础开销
// Token transfer:        ~20,000 CU
// 创建账户:               ~10,000 CU

CU 优化技巧

// 技巧 1:减少日志输出
pub fn optimized_operation(ctx: Context<Op>) -> Result<()> {
    // ❌ msg! 消耗较多 CU
    msg!("Processing user: {}", ctx.accounts.user.key());
    msg!("Amount: {}", amount);
    msg!("Timestamp: {}", Clock::get()?.unix_timestamp);

    // ✅ 生产环境移除不必要的日志
    // 或使用 emit! 事件(更便宜且可索引)
    emit!(ProcessEvent {
        user: ctx.accounts.user.key(),
        amount,
    });

    Ok(())
}

// 技巧 2:避免不必要的计算
pub fn calculate_reward(staked: u64, rate: u64, duration: u64) -> u64 {
    // ❌ 不必要的中间变量和类型转换
    let staked_f64 = staked as f64;
    let rate_f64 = rate as f64 / 10000.0;
    let duration_f64 = duration as f64;
    let reward = staked_f64 * rate_f64 * duration_f64;
    reward as u64

    // ✅ 直接用整数运算
    // staked * rate * duration / 10000
    // 注意溢出:先用 u128
    ((staked as u128) * (rate as u128) * (duration as u128) / 10000) as u64
}

// 技巧 3:批量操作减少 CPI 次数
pub fn batch_transfer(
    ctx: Context<BatchTransfer>,
    amounts: Vec<u64>,
) -> Result<()> {
    // ❌ 每次转账一个 CPI 调用(每次 ~20K CU)
    for (i, amount) in amounts.iter().enumerate() {
        // 每次 CPI 调用有固定开销
        token::transfer(/* ... */)?;
    }

    // ✅ 如果可能,合并为一次操作
    let total: u64 = amounts.iter().sum();
    token::transfer(/* total amount */)?;

    Ok(())
}

// 技巧 4:使用 init_if_needed 避免额外的 init 交易
#[derive(Accounts)]
pub struct CreateOrUpdate<'info> {
    #[account(
        init_if_needed,  // 如果账户不存在则创建
        seeds = [b"user_data", user.key().as_ref()],
        bump,
        payer = user,
        space = 8 + UserData::INIT_SPACE,
    )]
    pub user_data: Account<'info, UserData>,

    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

// 技巧 5:使用 Box 堆分配大结构体
#[derive(Accounts)]
pub struct LargeContext<'info> {
    // 栈大小有限(4KB),大账户用 Box 分配到堆上
    #[account(mut)]
    pub large_account: Box<Account<'info, LargeStruct>>,
}

4. 并行交易执行 — Sealevel 模型

Solana 的核心优势之一是并行交易执行。交易必须预先声明所有读写账户,运行时据此判断哪些交易可以并行:

交易并行规则:
  - 两笔交易读/写不同的账户 → 可以并行
  - 两笔交易都只读同一个账户 → 可以并行
  - 两笔交易有一笔写某账户,另一笔也访问该账户 → 必须串行

示例:
  TX1: 写入 Account A, 读取 Account B
  TX2: 写入 Account C, 读取 Account D
  → TX1 和 TX2 可以并行!(无重叠账户)

  TX3: 写入 Account A
  TX4: 读取 Account A
  → TX3 和 TX4 必须串行(Account A 冲突)

为并行设计的数据架构

// ❌ 差的设计:全局状态账户,所有交易都要写它 → 串行瓶颈
#[account]
pub struct GlobalPool {
    pub total_staked: u64,
    pub reward_per_token: u128,
    pub last_update_time: i64,
    // 所有 stake/unstake 都要修改这个账户
    // 导致所有交易串行化!
}

// ✅ 好的设计:每个用户独立账户 → 不同用户的操作可以并行
#[account]
pub struct UserStake {
    pub user: Pubkey,
    pub amount: u64,
    pub reward_debt: u128,
    pub last_update_time: i64,
}

// ✅ 进阶设计:分片(Sharding)减少写冲突
// 将全局状态分成多个分片,不同用户写不同分片
#[account(zero_copy)]
#[repr(C)]
pub struct PoolShard {
    pub shard_id: u8,
    pub total_staked: u64,
    pub reward_accumulated: u128,
}

// 用户根据地址哈希分配到不同分片
pub fn get_shard_id(user: &Pubkey) -> u8 {
    let bytes = user.to_bytes();
    bytes[0] % NUM_SHARDS // 按地址第一个字节取模
}

交易打包优化

// TypeScript 客户端优化

import {
    Transaction,
    ComputeBudgetProgram,
    AddressLookupTableAccount,
    VersionedTransaction,
    TransactionMessage,
} from "@solana/web3.js";

// 优化 1:合理设置 Compute Unit 限制和优先费
const modifyComputeUnits = ComputeBudgetProgram.setComputeUnitLimit({
    units: 300_000, // 根据实际需要设置,而非默认 200K
});

const addPriorityFee = ComputeBudgetProgram.setComputeUnitPrice({
    microLamports: 1_000, // 优先费(微 lamports/CU)
});

// 优化 2:使用 Address Lookup Tables 节省交易空间
// 每个完整地址 32 bytes,LUT 引用只需 1 byte index
const lookupTableAccount = await connection.getAddressLookupTable(
    lookupTableAddress
);

// 优化 3:使用 Versioned Transactions
const messageV0 = new TransactionMessage({
    payerKey: payer.publicKey,
    recentBlockhash: blockhash,
    instructions: [
        modifyComputeUnits,
        addPriorityFee,
        mainInstruction,
    ],
}).compileToV0Message([lookupTableAccount.value!]);

const transaction = new VersionedTransaction(messageV0);

// 优化 4:批量发送不冲突的交易
async function sendParallelTransactions(
    transactions: VersionedTransaction[]
) {
    // 并行发送,Solana 会并行执行不冲突的交易
    const promises = transactions.map(tx =>
        connection.sendTransaction(tx, { skipPreflight: true })
    );
    const signatures = await Promise.all(promises);
    return signatures;
}

5. 优化 Vault 程序实战

use anchor_lang::prelude::*;

declare_id!("OPtm111111111111111111111111111111111111111");

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

    pub fn initialize(ctx: Context<Initialize>, max_depositors: u16) -> Result<()> {
        let vault = &mut ctx.accounts.vault.load_init()?;
        vault.authority = ctx.accounts.authority.key();
        vault.total_deposited = 0;
        vault.depositor_count = 0;
        vault.max_depositors = max_depositors;
        vault.bump = ctx.bumps.vault;
        Ok(())
    }

    pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
        require!(amount > 0, VaultError::ZeroAmount);

        let vault = &mut ctx.accounts.vault.load_mut()?;
        let depositor_key = ctx.accounts.depositor.key();

        // 查找或创建存款记录
        let mut found = false;
        for i in 0..vault.depositor_count as usize {
            if vault.depositors[i].pubkey == depositor_key {
                // 已存在,更新余额
                vault.depositors[i].balance = vault.depositors[i].balance
                    .checked_add(amount)
                    .ok_or(VaultError::Overflow)?;
                found = true;
                break;
            }
        }

        if !found {
            let idx = vault.depositor_count as usize;
            require!(idx < vault.max_depositors as usize, VaultError::VaultFull);
            vault.depositors[idx] = DepositorInfo {
                pubkey: depositor_key,
                balance: amount,
                deposit_time: Clock::get()?.unix_timestamp,
            };
            vault.depositor_count += 1;
        }

        vault.total_deposited = vault.total_deposited
            .checked_add(amount)
            .ok_or(VaultError::Overflow)?;

        // CPI: SPL Token 转账
        anchor_spl::token::transfer(
            CpiContext::new(
                ctx.accounts.token_program.to_account_info(),
                anchor_spl::token::Transfer {
                    from: ctx.accounts.depositor_token.to_account_info(),
                    to: ctx.accounts.vault_token.to_account_info(),
                    authority: ctx.accounts.depositor.to_account_info(),
                },
            ),
            amount,
        )?;

        Ok(())
    }
}

/// Zero-Copy Vault - 支持大量存款人
#[account(zero_copy)]
#[repr(C)]
pub struct VaultState {
    pub authority: Pubkey,       // 32
    pub total_deposited: u64,    // 8
    pub depositor_count: u16,    // 2
    pub max_depositors: u16,     // 2
    pub bump: u8,                // 1
    pub _padding: [u8; 3],       // 3 (对齐到 8 字节)
    pub depositors: [DepositorInfo; 128], // 128 * 48 = 6144
}
// 总计: 32 + 8 + 2 + 2 + 1 + 3 + 6144 = 6192 bytes

#[zero_copy]
#[repr(C)]
pub struct DepositorInfo {
    pub pubkey: Pubkey,          // 32
    pub balance: u64,            // 8
    pub deposit_time: i64,       // 8
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(
        init,
        seeds = [b"vault_v2", authority.key().as_ref()],
        bump,
        payer = authority,
        // zero-copy 账户需要指定完整大小
        space = 8 + std::mem::size_of::<VaultState>(),
    )]
    pub vault: AccountLoader<'info, VaultState>,

    #[account(mut)]
    pub authority: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Deposit<'info> {
    #[account(
        mut,
        seeds = [b"vault_v2", vault.load()?.authority.as_ref()],
        bump = vault.load()?.bump,
    )]
    pub vault: AccountLoader<'info, VaultState>,

    #[account(mut)]
    pub depositor: Signer<'info>,

    #[account(mut)]
    pub depositor_token: Account<'info, anchor_spl::token::TokenAccount>,

    #[account(mut)]
    pub vault_token: Account<'info, anchor_spl::token::TokenAccount>,

    pub token_program: Program<'info, anchor_spl::token::Token>,
}

#[error_code]
pub enum VaultError {
    #[msg("Amount must be > 0")]
    ZeroAmount,
    #[msg("Arithmetic overflow")]
    Overflow,
    #[msg("Vault is full")]
    VaultFull,
}

关键要点总结

优化方向方法效果
数据反序列化Zero-Copy (#[zero_copy])CU 减少 90%+
日志输出减少 msg!,使用 emit!CU 减少
数学计算整数运算代替浮点CU 减少,精度可控
CPI 调用批量操作减少 CPI 次数每次 CPI 节省 ~10K CU
并行执行分片设计减少写冲突吞吐量线性增长
交易大小Address Lookup Table节省交易空间
栈空间Box 分配大结构体避免栈溢出

常见误区

  1. "Solana 不需要优化因为它够快" — 错误!CU 限制是硬性约束,复杂逻辑很容易超限
  2. "Zero-Copy 适用于所有场景" — 不对。小账户(< 1KB)用标准 Borsh 更简单,Zero-Copy 对字段类型有严格限制
  3. "并行 = 自动更快" — 只有不冲突的交易才能并行。全局状态是串行瓶颈
  4. "交易越大越好" — Solana 交易有 1232 bytes 硬限制,必须精心打包
  5. "CU 费用 = Gas 费用" — CU 消耗不直接决定费用。Solana 的基础费用是固定的,CU 优先费是可选的

面试关联

面试题:Solana 如何实现并行交易执行?对开发者有什么影响?

30 秒回答: Solana 的 Sealevel 引擎要求交易预先声明所有读写账户。如果两笔交易没有写冲突,就可以并行执行。这要求开发者在数据架构设计时避免全局状态瓶颈,通过分片等方式减少写冲突。

2 分钟回答: Solana 的 Sealevel 运行时是目前唯一实现智能合约并行执行的主流虚拟机。核心机制是交易必须在提交时声明所有访问的账户及读写模式。运行时据此构建依赖图:读-读不冲突、读-写和写-写必须串行。对开发者的影响是巨大的——数据架构设计直接决定协议吞吐量。如果所有操作都需要修改同一个全局状态账户,那所有交易实际上是串行的。好的设计应该将状态分散到用户级别的 PDA 账户,或者使用分片模式将全局状态分成多个分片。此外,Zero-Copy 反序列化可以大幅减少 CU 消耗,在大账户场景下节省超过 90% 的计算资源。


参考资源

资源说明
Anchor Zero-Copy 文档零拷贝官方文档
Solana Compute BudgetCU 和费用说明
Sealevel 并行执行Sealevel 原理
Address Lookup TablesALT 文档
Solana 性能优化指南官方优化指南