程序优化通常被说成"要装进计算上限"。那是地板,不是目标。
★优先费 = compute unit price × compute unit limit。你的程序需要的每一个单元,都是你的调用方在每一笔交易上要付的单元 —— 而且是按那一分钟的市场价。★
让这件事要紧的那个算术
优先费 = CU 价格 × CU 上限
调用方必须请求一个高到足够让你的程序跑完的上限。★如果你的指令需要 120,000 单元而不是 60,000,他们为同一笔交易付双倍优先费★ —— 而在拥堵时,当每单元的价格飙升,这个差距在绝对值上会拉得更开。
这就是为什么计算优化是一个面向用户的决策,而不是一件内部事务。 你的程序效率显示在别人的手续费账单上,而他们没法从自己那边修好它。
这些单元到底花在哪
测量胜过猜测,而运行时会直接告诉你:
use solana_program::log::sol_log_compute_units;
sol_log_compute_units(); // ★之前★
do_the_expensive_thing()?;
sol_log_compute_units(); // ★之后★
典型的分布会让人意外:
| 开销 | 大致权重 |
|---|---|
| ★账户反序列化★ | ★常常是最大的单项★ |
| ★CPI 开销★ | ★每次调用都不小★ |
| 算术 | 通常可忽略 |
★msg! 日志★ |
★真实存在,而且容易忘★ |
| 账户校验 | 中等,但会累加 |
★带星的前两项才是收益所在。★ 一边在抠算术、一边反序列化五个你从没读过的账户,是在优化错的那一端。
如果你的程序已经很精简、交易还是会错过,免费领个 BoltTx key 你的用户改一行就能测测提交路径。
反序列化是第一个该看的地方
Anchor 会在你的指令体运行之前反序列化 context 里的每一个账户 —— 不管你用不用:
#[derive(Accounts)]
pub struct Swap<'info> {
pub pool: Account<'info, Pool>, // ★被反序列化★
pub metadata: Account<'info, Metadata>, // ★用不到也被反序列化★
/// CHECK: 只需要地址
pub reference: UncheckedAccount<'info>, // ★不反序列化★
}
★一个你只需要它地址的账户,不该是一个带类型的 Account。★ UncheckedAccount 加上你自己的地址检查,花的是一次比较,而不是一次完整的反序列化。
对那些"很大但你只要一个字段"的账户,零拷贝能避免把整个结构体实例化出来:
pub pool: AccountLoader<'info, LargePool>, // ★借用,不反序列化★
这个取舍是真实的: UncheckedAccount 把校验从框架挪到了你身上,忘掉一个检查是安全 bug,不是性能问题。★只在账户确实除了地址之外不需要任何校验时才用它。★
日志比看起来更贵
msg!("swap: user={} amount={}", ctx.accounts.user.key(), amount); // ★格式化 + 写入★
程序内部的字符串格式化会消耗单元,而且它在每一次调用时都发生 —— 包括那几千次根本没人看日志的调用。
★记录你调试失败所需要的,不是你在一切正常时想看到的。★ 交易 meta 已经记录了余额变化,所以记录金额是在重复链免费提供的信息。
CPI 不是免费的
每一次跨程序调用都带着被调程序开销之外的建立成本:
// ★两次 CPI:两份开销。★
token::transfer(ctx_a, amount_a)?;
token::transfer(ctx_b, amount_b)?;
接口允许的地方就合并,并且要问一句:某个中间 CPI 做的事,你能不能直接算出来。 ★一个为了代码整洁而存在的包装程序,在每一笔交易上都让你的用户付钱。★
深度也要算:你的 CPI 占掉四层嵌套里的一层,而经聚合器路由过来的调用方,已经花掉了两三层。
把你的数字公布出来
★这是几乎没有程序会做、但对调用方价值最高的一件事。★
swap_exact_in 约 48,000 CU
add_liquidity 约 62,000 CU
close_position 约 31,000 CU
一个知道你的指令要 48,000 单元的调用方,会把上限设成大约 58,000,并按此付费。而一个不知道的调用方会默认 200,000,付出超出必要四倍的钱 —— 然后把高手续费怪在网络头上。
公布实测数字不花你任何成本,却直接减少你用户的开销。 ★它同时也是一个信号:你确实测量过。★
优化解决不了什么
值得说清楚,好让力气花在对的地方:
它不会让交易更快落块。 ★计算影响的是手续费,不是路由。★ 一条精简的指令仍然和其它交易一样争夺收录。
在噪音水平以下没用。 在一笔 200,000 单元的交易上省 2,000 单元,改变很小。
它修不好一个错的上限。 ★如果调用方为一条 48,000 单元的指令请求 200,000 单元,你的优化对他们一点忙都没帮上★ —— 这就是为什么公布这个数字和减少它同样重要。
上链应该是什么水平
我们投递节点上的真实交易:中位确认 336 毫秒 —— 不到一个 slot。
★计算决定一笔交易花多少钱;路由决定它什么时候落块。★ 两者对你的用户都重要,而它们是在不同地方解决的两个独立问题。
BoltTx 在这里的位置
我们为调用你程序的那些人处理提交。计算消耗由你的程序和他们的 compute budget 指令决定。
提交走我们自建的四区域投递节点,带 SWQoS 路由,不暴露公共 mempool —— 交易在落块之前、传输途中观察不到。我们不修改交易内容,这包括绝不改动 compute budget 指令。
你的调用方在本地签名。我们不托管资金、不代签、不修改交易内容。tip 带在交易里、从他们自己的钱包链上支付,交易失败时跟着一起 revert —— 这是 Solana 原子交易的机制决定的。只有到达链上的交易才计费。
免费领 API key,没有月费:
const connection = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");
常见问题
我的 Solana 程序消耗多少计算,为什么对用户要紧? 因为优先费是 compute unit 价格乘以请求的上限。你的指令需要的每一个单元都是调用方要付的,而他们没法从自己那边减少它。
怎么测量 Solana 程序的计算消耗?
在一段代码前后各调一次 sol_log_compute_units,从日志读差值。测量胜过估算,因为实际分布很少符合直觉。
典型程序里什么最耗计算? 账户反序列化和 CPI 开销,通常远超算术。一边抠数学、一边反序列化用不到的账户,是在优化错的那一端。
怎么避免反序列化用不到的账户?
只需要地址时,用 UncheckedAccount 加你自己的地址检查。代价是校验从框架变成了你的责任。
什么时候该用零拷贝账户?
大账户但你只要几个字段时。 AccountLoader 借用数据而不把整个结构体实例化出来,完全避开了反序列化成本。
msg! 日志消耗计算吗? 消耗,包括字符串格式化。它在每一次调用时都跑,不管有没有人看输出 —— 所以记录能帮你调试失败的,而不是好看的。
一次 CPI 有多贵? 它带着被调程序开销之外的建立成本,而且占掉四层嵌套里的一层。在接口允许的地方合并,能同时降低两项成本。
该公布程序的计算成本吗? 该。知道一条指令要 48,000 单元的调用方会设一个合适的上限,而不知道的会默认 200,000、多付大约四倍。
减少计算能让交易更快落块吗? 不能。计算影响手续费,不影响路由。 一条精简的指令和其它交易一样争夺收录,落块速度是另一个问题。
一笔交易的计算上限是多少? 有一个所有指令(包括 CPI)共享的单笔预算。调用方显式请求上限,而请求超出所需会按比例抬高他们的手续费。
我的用户为什么抱怨手续费高? 可能是因为不管你的指令需要多少,他们都默认 200,000 单元的上限。公布实测数字,往往比优化本身更能降低他们的成本。
算术值得优化吗? 很少值得。整数运算相对反序列化和 CPI 很便宜。 优化之前先测量,因为关于"单元花在哪"的直觉通常是错的。
一条指令能用多少计算? 最多到这笔交易请求的上限,而且要和里面每一条其它指令和 CPI 共享。一个吃掉大部分预算的程序,会限制调用方能围绕它组合什么。
Anchor 会增加计算开销吗? 会一些,主要来自自动的账户反序列化和校验。为了安全这是个合理的交换,而且在账户确实不需要校验的地方,这份成本是可以减掉的。
该在上线前还是上线后优化? 上线前先测量,这样你能公布准确数字。 优化本身可以之后再做,但调用方从第一天起就需要这些数字来正确设置上限。
调用方怎么用我公布的计算数字?
把 setComputeUnitLimit 设成你的数字加一点余量,这会按比例降低他们的优先费,同时不冒计算耗尽失败的风险。
延伸阅读
- Solana compute unit 计价
- Solana 的 CPI 深度上限
- Solana 交易模拟
- Solana 账户大小规划
- Solana 交易上链完全指南