Fuel Core VM Gas 计量基准实测:从 cargo criterion 到 collect 生成 gas-costs 全流程解析
【免费下载链接】fuel-coreRust full node implementation of the Fuel v2 protocol.项目地址: https://gitcode.com/GitHub_Trending/fu/fuel-core
本文围绕benches/README.md及其配套的 fuel-core-benches 基准代码 展开,讲解 Fuel Core 节点如何为 Fuel v2 协议中的 VM 指令计算 gas 计量成本:如何运行基准套件、如何用 flamegraph 验证测量对象、以及如何用仓库自带的collect工具把 criterion 的 JSON 输出转化为gas-costs.yaml或可直接替换进GasCostsValuesV7的default-gas-costs.rs。读完后,你可以独立完成一次"跑基准 → 采集数据 → 生成 gas 成本文件"的完整流程。
基准目录的定位与结构
benches/README.md开篇即说明了这套基准的核心用途:为 Fuel Core VM 计算 gas 计量成本(gas metering costs)。换句话说,链上每条 VM 指令消耗多少 gas,并不是拍脑袋定的,而是由这组基准实测得出。
从源码结构看,benches/目录同时承担两个角色:
- 基准测试入口:benches/benches/ 下是 criterion 基准,按功能域拆分为
vm_set/(alu、blockchain、crypto、flow、mem 五大指令族)、contract.rs(contract_root / state_root)、vm_initialization.rs(VM 初始化成本)等模块; - 可复用的基准工具库:benches/src/lib.rs 以库形式导出
VmBench、default_gas_costs、import等模块,供各基准二进制共享。
benches/Cargo.toml 中注册了 6 个[[bench]]目标,全部harness = false(即由 criterion 自己驱动,而非 libtest):
| 基准二进制 | 测量对象(从源码结构看) |
|---|---|
vm | VM 指令级成本,即 gas 成本的数据来源 |
block_target_gas | 区块目标 gas 相关场景(block_target_gas_set/) |
import | 区块导入性能(benches/src/import.rs) |
state | 状态存储性能 |
transaction_throughput | 交易吞吐 |
db_lookup_times | 数据库查询耗时(配套 db_lookup_times_utils/) |
其中defaultfeature 启用了fuel-core/rocksdb与rocksdb-production,说明基准默认在 RocksDB 存储后端下运行,贴近生产节点的存储路径。
运行基准:cargo bench 与 cargo criterion
benches/README.md给出的运行方式非常直接,两种等价选择:
# 方式一:直接用 cargo cargo bench -p fuel-core-benches # 方式二:使用 criterion 命令行工具(需单独安装) cargo criterion -p fuel-core-benches两者的差异在于:cargo bench会编译并运行上面列出的全部 6 个基准二进制,适合完整回归;cargo criterion则更灵活,支持--bench过滤单个基准、--filter过滤组内单个 benchmark、以及 JSON 消息输出(后文生成 gas 数据时会用到)。
一个值得注意的实现细节在 benches/benches/vm.rs 中:基准通过#[global_allocator]把全局分配器切换为tikv_jemallocator::Jemalloc,以保证计时不受系统分配器抖动影响。更关键的是run_group_ref中对测量循环的设计:
- 每次迭代前,为 VM 构造三层嵌套的存储事务(block → relayer → tx),并注释明确说明这是为了"模拟区块生产/验证时的真实嵌套层级,保持相同性能特征";
- 用
quanta::Clock精确计时,并对指令执行前后调用black_box防止编译器优化掉被测代码; - 每轮迭代结束后通过
vm.reset_vm_state(diff)与数据库reset_changes()把 VM 和存储回滚到初始状态,保证每次迭代都从完全相同的前置状态执行同一条指令——这正是"单指令成本"能独立于业务状态被准确测量的原因。
用 flamegraph 剖析单个基准
README 指出,有时需要为某个基准生成火焰图(flamegraph)来验证自己确实在测量正确的东西。完整步骤如下(全部来自 benches/README.md):
第一步:带调试符号构建基准二进制
CARGO_PROFILE_RELEASE_DEBUG=true cargo-criterion -p fuel-core-benches --no-runCARGO_PROFILE_RELEASE_DEBUG=true让 release 构建保留调试符号,这是 flamegraph 能还原出函数名的前提。
第二步:找到刚构建出的基准二进制
ls target/release/deps/vm* -lat | head -1输出形如:
target/release/deps/vm-a17190f2ca5e7169第三步:用 flamegraph 运行指定基准(--profile-time控制剖析时长,示例为 10 秒)
# 剖析某个 op,--bench 参数是正则 flamegraph target/release/deps/vm-a17190f2ca5e7169 --bench '^swwq/swwq$' --profile-time 10 # 剖析依赖型基准(带参数的组内成员) flamegraph target/release/deps/vm-a17190f2ca5e7169 --bench '^srwq/100$' --profile-time 10这里的基准 ID 采用组名/成员名的层级结构,例如swwq/swwq、srwq/100——这与 benches/src/bin/collect.rs 测试用例中mcli/10000、mcp/100000等 ID 的形态一致:组名对应一个指令或一组相似指令,斜杠后的数字是该组内某个具体数据点(如操作数长度、slot 大小)的 ID。用正则锚定(^...$)可以确保只剖析目标基准,避免 10 秒剖析时间被同组其他基准稀释。
使用 collect 生成基准数据
这一步是把"原始计时"转化为"gas 成本"的核心链路,README 分两步:
1. 运行基准并保存 JSON 输出
cargo criterion -p fuel-core-benches --message-format json --bench vm > bench.json--message-format json让 criterion 把每完成一个基准/一个组的事件以 JSON 行输出到 stdout。README 特别提醒:不要直接把管道接给 collect 二进制,基准运行很慢,建议先落盘保存(如上例的bench.json)。
2. 运行 collect 生成 gas 成本文件
cargo run -p fuel-core-benches --bin collect --release -- --input bench.json运行后会在当前目录生成gas-costs.yaml,形如:
burn: 35 call: base: 311 dep_per_unit: 14从 collect 的源码 可以看到,collect是一个 clap 命令行工具,完整参数比 README 展示的更丰富:
| 参数 | 说明 |
|---|---|
-b, --baseline | 用作基线的 benchmark ID,默认noop/noop |
-i, --input | criterion JSON 输入路径,支持单文件或整个目录(目录内所有文件都会读取),缺省为 stdin |
-o, --output | 输出路径,默认当前目录;若给的是目录则按格式自动命名 |
-f, --format | 输出格式:yaml(默认)/json/rust/consensus-parameters |
-d, --debug | 打印输入行与解析出的状态 |
-a, --all | 对依赖型测量保留所有样本值(不能与rust格式共用) |
对应地,输出文件按格式自动命名为gas-costs.yaml、gas-costs.json、gas-costs.rs或consensus-parameters.rs(见 collect.rs 的 main 函数)。
成本是如何算出来的:从 noop 基线到 DependentCost
理解collect的内部逻辑,能解释为什么 README 示例中burn: 35这样的数字代表"35 个 noop 的耗时"。其核心流程在 collect.rs 中:
基线归一化。每个组(group)中若存在带 throughput 的成员,会被视为"依赖型"基准;否则取组内第一个成员的平均耗时,除以基线耗时得到相对成本。基线默认是noop/noop,其取值逻辑见get_baseline——若录制中找不到该基线会直接 panic,提示你确认录制的完整性。
依赖型成本的分型拟合。dependent_cost函数先用最小二乘线性回归求出x/y(每单位 noop 能处理多少元素),然后依据首尾数据点的分布把曲线分成三类:
Linear:首尾点斜率都接近回归线(偏差 < 20%),取首点耗时为base、取最小amount为每单位成本;Logarithm/Exp:对数型取拐点后的线性段做基准,指数型则打印警告"不支持指数曲线,指令需要设置上界";- 最终若
amount > 1输出DependentCost::LightOperation { base, units_per_gas },否则输出HeavyOperation { base, gas_per_unit }。
存储基准的特殊处理。从源码看,swrd_*、srdd_*(存储写/读)两组还会把小于 32 字节的 slot 样本排除在回归之外(MIN_SLOT_SIZE_BYTES),避免极小 slot 的固定开销扭曲每字节斜率;并且会做冷热一致性 sanity check——例如冷读必须比热读贵、冷写不应比热写便宜,异常时向 stderr 打印警告。这些组名最终通过STORAGE_BENCH_REMAPS映射到GasCostsValuesV7的storage_read_hot、storage_read_cold、storage_write、storage_clear字段。
Rust 代码生成。以-f rust输出时,to_rust_code会:把mod、move等与 Rust 关键字冲突的名称重映射为mod_op、move_op,把ret_contract等收敛为统一的ret/rvrt/retd;对 YAML 中出现但GasCostsValuesV7未覆盖的键打印告警;并在文件头写入生成时的 git commit hash(git rev-parse HEAD)以便追溯。
生成 default-gas-costs.rs 并替换
README 的最后一个小节说明了如何把基准结果固化进代码:
cargo run -p fuel-core-benches --bin collect --release -- --input bench.json -f rust --output default-gas-costs.rs这会按上面的模板在当前目录生成default-gas-costs.rs,内容是一个pub fn default_gas_costs() -> GasCostsValues函数,逐字段填充GasCostsValuesV7。README 说明确认无误后可以替换fuel-vm/src/gas/default-gas-costs.rs;在当前仓库中,对应角色由 benches/src/default_gas_costs.rs 承担——它以GasCostsValuesV7字面量给出当前默认成本,其中既能看到相对成本(如burn: 867、ecr1: 31208),也能看到依赖型成本(如call: DependentCost::LightOperation { base: 780, units_per_gas: 65 }、storage_read_cold: { base: 733, units_per_gas: 31 }),与collect的输出结构一一对应。
小结
这套基准体系的分工很清晰:benches/benches/vm.rs 负责在"状态每轮回滚、分配器固定、存储嵌套层级贴近生产"的条件下精确计时单条 VM 指令;collect 工具 负责以noop为基线把纳秒级均值换算为相对 gas 成本,并对依赖型指令做线性/对数分型拟合与冷热一致性检查;最终产物gas-costs.yaml或default-gas-costs.rs就是 VM gas 计量参数的数据源头。掌握cargo criterion --message-format json+collect -f rust这条链路,你就能在修改指令实现或存储路径后,量化其对 gas 成本的影响并复现一次完整的参数刷新流程。
【免费下载链接】fuel-coreRust full node implementation of the Fuel v2 protocol.项目地址: https://gitcode.com/GitHub_Trending/fu/fuel-core
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考