跳转到内容

计算交易 Gas

Aptos 交易默认会收取基础 Gas 费,不受市场状况影响。每笔交易的“基础 Gas”取决于三项因素:

  1. 指令。
  2. 存储。
  3. 载荷。

交易需要的函数调用、条件分支等越多,指令 Gas 越高;对全局存储的读取和写入越多,存储 Gas 越高;交易载荷中的字节数越多,成本也越高。

优化原则所述,存储 Gas 对基础 Gas 的影响远大于其他部分。有关 Aptos Gas 模型的背景,请参阅 The Making of the Aptos Gas Schedule

基础指令 Gas 参数定义于 instr.rs,包括以下指令类型。

参数含义
nop空操作
参数含义
ret返回
abort中止
br_true执行条件为真的分支
br_false执行条件为假的分支
branch跳转
参数含义
pop从栈中弹出
ld_u8加载 u8
ld_u16加载 u16
ld_u32加载 u32
ld_u64加载 u64
ld_u128加载 u128
ld_256加载 u256
ld_true加载 true
ld_false加载 false
ld_const_base加载常量的基础成本
ld_const_per_byte按字节计算的常量加载成本
参数含义
imm_borrow_loc不可变借用
mut_borrow_loc可变借用
imm_borrow_field不可变借用字段
mut_borrow_field可变借用字段
imm_borrow_field_generic泛型字段的不可变借用
mut_borrow_field_generic泛型字段的可变借用
copy_loc_base复制的基础成本
copy_loc_per_abs_val_unit按抽象值单元计算的复制成本
move_loc_base移动
st_loc_base存储到局部位置
参数含义
call_base函数调用的基础成本
call_per_arg每个函数参数的成本
call_per_local每个局部参数的成本
call_generic_base泛型调用基础成本
call_generic_per_ty_arg每个类型参数的成本
call_generic_per_arg每个泛型参数的成本
call_generic_per_local每个泛型局部参数的成本
参数含义
pack_base打包 struct 的基础成本
pack_per_field每个字段的打包成本
pack_generic_base泛型打包基础成本
pack_generic_per_field每个泛型字段的打包成本
unpack_base解包 struct 的基础成本
unpack_per_field每个字段的解包成本
unpack_generic_base泛型解包基础成本
unpack_generic_per_field每个泛型字段的解包成本
参数含义
read_ref_base从引用读取的基础成本
read_ref_per_abs_val_unit按抽象值单元计算的读取成本
write_ref_base写入引用的基础成本
freeze_ref冻结引用
参数含义
cast_u8转换为 u8
cast_u16转换为 u16
cast_u32转换为 u32
cast_u64转换为 u64
cast_u128转换为 u128
cast_u256转换为 u256
参数含义
add
sub
mul
mod_取模
div
参数含义
bit_orOR:|
bit_andAND:&
xorXOR:^
shl左移:<<
shr右移:>>
参数含义
orOR:||
andAND:&&
notNOT:!
参数含义
lt小于:<
gt大于:>
le小于或等于:<=
ge大于或等于:>=
eq_base相等比较基础成本:==
eq_per_abs_val_unit按抽象值单元计算的相等比较成本
neq_base不相等比较基础成本:!=
neq_per_abs_val_unit按抽象值单元计算的不相等比较成本
参数含义
imm_borrow_global_base不可变全局借用 borrow_global<T>() 的基础成本
imm_borrow_global_generic_base泛型不可变全局借用基础成本
mut_borrow_global_base可变全局借用 borrow_global_mut<T>() 的基础成本
mut_borrow_global_generic_base泛型可变全局借用基础成本
exists_base检查 exists<T>() 的基础成本
exists_generic_base泛型存在性检查基础成本
move_from_basemove_from<T>() 的基础成本
move_from_generic_base泛型 move_from 基础成本
move_to_basemove_to<T>() 的基础成本
move_to_generic_base泛型 move_to 基础成本
参数含义
vec_len_base向量长度
vec_imm_borrow_base不可变借用元素
vec_mut_borrow_base可变借用元素
vec_push_back_base在尾部追加
vec_pop_back_base从尾部弹出
vec_swap_base交换元素
vec_pack_base打包向量的基础成本
vec_pack_per_elem每个元素的向量打包成本
vec_unpack_base解包向量的基础成本
vec_unpack_per_expected_elem每个元素的向量解包成本

其他存储 Gas 参数定义于 table.rsmove_stdlib.rs 以及 aptos-gas-schedule/src/ 下的其他源文件。

下列 Gas 参数用于表示临时存储设备资源(包括磁盘 IOPS 和带宽)的成本:

在 Move 中可以从全局状态读取数据:

参数含义
storage_io_per_state_slot_read每个从全局状态加载的条目收费
storage_io_per_state_byte_read每个从全局状态加载的字节收费

对于已提交交易,会有多项内容写入账本历史。账本历史可被裁剪,并不占用永久磁盘空间,因此不会收取存储费用。

参数含义
storage_io_per_state_slot_write按交易输出中的每次状态写操作收费
storage_io_per_state_byte_write按交易输出中所有状态写操作的每个字节收费
storage_io_per_event_byte_write按交易输出中所有事件的每个字节收费
storage_io_per_transaction_byte_write按交易本身(也是账本历史的一部分)的每个字节收费

下列存储费参数以绝对 APT 数额收取,用于表示使用 Aptos 已认证数据结构在链上存储条目所产生的磁盘空间和结构成本。

参数含义
storage_fee_per_state_slot_create通过 move_to()、table::add() 等分配状态槽
storage_fee_per_state_byte每次槽位大小增长都会收费,而不仅在分配时收费;为简化处理,退款只在删除时发生。详见 AIP-65

向量也按字节收取费用。它消耗 _i=0n1e_i+b(n)\sum\_{i = 0}^{n - 1} e\_i + b(n) 字节,其中:

  • nn 是向量中的元素数量。
  • e_ie\_i 是元素 ii 的大小。
  • b(n)b(n)nn 的函数,即“基础大小”。

有关向量基础大小(技术上为 ULEB128),请参阅 BCS 序列规范。在实践中它通常只占一个字节,因此包含 100 个 u8 元素的向量占用 100+1=101100 + 1 = 101 字节;按前述逐项读取方法,读取最后一个元素会被视为读取 101 字节。

载荷 Gas 定义于 transaction.rs,它将存储 Gas 与若干载荷和定价参数结合:

参数含义
min_transaction_gas_units交易的最小内部 Gas 单位数,在执行开始时收取
large_transaction_cutoff超过该字节数后,交易按每字节收取额外费用
intrinsic_gas_per_byte载荷超过 large_transaction_cutoff 后每字节收取的内部 Gas 单位
maximum_number_of_gas_units一笔交易可使用的外部 Gas 单位上限
min_price_per_gas_unit交易允许的最低 Gas 单价
max_price_per_gas_unit交易允许的最高 Gas 单价
max_transaction_size_in_bytes最大交易载荷大小(字节)
gas_unit_scaling_factor内部 Gas 单位和外部 Gas 单位的换算因子

“内部 Gas 单位”由 instr.rs、storage_gas.move 等源文件定义;其粒度相对于“外部 Gas 单位”高 gas_unit_scaling_factor 倍。将内部单位除以 gas_unit_scaling_factor 即可得到外部单位;再乘以 Gas 价格(每单位外部 Gas 对应的 octa 数)即可换算为 octa

在撰写本文时,transaction.rs 中的 min_price_per_gas_unit 定义为 aptos_global_constants::GAS_UNIT_PRICE(即 100)。其他值得注意的常量如下:

常量
min_price_per_gas_unit100
max_price_per_gas_unit10,000,000,000
gas_unit_scaling_factor1,000,000

这些常量的含义见载荷 Gas

网络负载较低时,Gas 单价通常较低,交易的大多数成本都会更可负担;但存储费是例外,因为它按绝对 APT 价值定价。交易创建状态槽、写入较大状态项、发出大量或很大的事件、或自身载荷很大时,存储费通常是总成本的主要部分;这些因素都会消耗 Aptos 节点磁盘空间并相应收费。

存储退款会激励删除不再使用的状态项以释放状态槽。释放槽位时会全额退还状态槽费用,但超出的状态字节费用不可退还。将来会通过区分永久字节(全局状态中的字节)和相对短暂字节(经过账本历史的字节)来调整这一机制。

关于存储费的优化策略:

  1. 尽量少创建状态项。
  2. 尽量少发出事件。
  3. 避免大型状态项、事件和交易。
  4. 清理不再使用的状态项。
  5. 若两个字段总是一起更新,将它们放入同一资源或资源组。
  6. 若结构体很大但只有少数字段频繁更新,将这些字段移至独立资源或资源组。

在撰写本文时,meter.rs 中定义的 EXECUTION_GAS_MULTIPLIER 为 20,所有指令 Gas 操作都会乘以该值。以下代表性操作的成本假定为:先将内部 Gas 除以缩放因子,再乘以最低 Gas 单价。

操作最低 octa
添加、借用或移除 Table box240
函数调用200
加载常量130
全局借用100
读写引用40
在栈上加载 u12816
每字节的 Table box 操作2

按字节计算的 Table box 指令 Gas 不包含单独计收的存储 Gas。作为对比,在最低价格下,读取 100 字节项目的成本为 r_i+100\*r_b=3000+100\*3=3300r\_i + 100 \* r\_b = 3000 + 100 \* 3 = 3300 octa,约为函数调用的 16.5 倍;总体而言,存储 Gas 通常远高于指令 Gas。

从技术上说,减少函数调用仍有激励,但工程工作更应着力于编写模块化、分解良好的代码来降低存储 Gas,而不是用重复代码块减少嵌套函数调用。极端情况下,例如循环数学函数需要 10,000 次迭代才收敛,指令 Gas 可能远高于存储 Gas;但对大多数应用,存储 Gas 影响更大。

在撰写本文时,transaction/mod.rs 将每笔交易的最小内部 Gas 定义为 1,500,000 单位(最低为 15,000 octa)。当载荷超过 600 字节时,每额外字节增加 2,000 内部 Gas 单位(最低 20 octa);交易允许的最大字节数为 65536。因此,实践中载荷 Gas 通常无需特别担忧。