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 分配大结构体 | 避免栈溢出 |
常见误区
- "Solana 不需要优化因为它够快" — 错误!CU 限制是硬性约束,复杂逻辑很容易超限
- "Zero-Copy 适用于所有场景" — 不对。小账户(< 1KB)用标准 Borsh 更简单,Zero-Copy 对字段类型有严格限制
- "并行 = 自动更快" — 只有不冲突的交易才能并行。全局状态是串行瓶颈
- "交易越大越好" — Solana 交易有 1232 bytes 硬限制,必须精心打包
- "CU 费用 = Gas 费用" — CU 消耗不直接决定费用。Solana 的基础费用是固定的,CU 优先费是可选的
面试关联
面试题:Solana 如何实现并行交易执行?对开发者有什么影响?
30 秒回答: Solana 的 Sealevel 引擎要求交易预先声明所有读写账户。如果两笔交易没有写冲突,就可以并行执行。这要求开发者在数据架构设计时避免全局状态瓶颈,通过分片等方式减少写冲突。
2 分钟回答: Solana 的 Sealevel 运行时是目前唯一实现智能合约并行执行的主流虚拟机。核心机制是交易必须在提交时声明所有访问的账户及读写模式。运行时据此构建依赖图:读-读不冲突、读-写和写-写必须串行。对开发者的影响是巨大的——数据架构设计直接决定协议吞吐量。如果所有操作都需要修改同一个全局状态账户,那所有交易实际上是串行的。好的设计应该将状态分散到用户级别的 PDA 账户,或者使用分片模式将全局状态分成多个分片。此外,Zero-Copy 反序列化可以大幅减少 CU 消耗,在大账户场景下节省超过 90% 的计算资源。
参考资源
| 资源 | 说明 |
|---|---|
| Anchor Zero-Copy 文档 | 零拷贝官方文档 |
| Solana Compute Budget | CU 和费用说明 |
| Sealevel 并行执行 | Sealevel 原理 |
| Address Lookup Tables | ALT 文档 |
| Solana 性能优化指南 | 官方优化指南 |