Aptos 链上的交易,在不考虑市场供需的情况下,会收取一笔“基础 Gas 费”。它是由以下3部分组成:
- 指令
- 存储
- 载荷
一笔交易含有越多的函数调用,分支判断之类的复杂逻辑,就消耗越多的“指令” Gas 。相应地,如果交易中有越多的读写请求,就消耗越多的“存储” Gas。而一笔交易上附带的载荷(payload)越长(字节数多),就消耗越多的“载荷” Gas。
如 优化原则 一节所描述的,在基础 Gas 费中,存储 Gas 占用的比重最大。
指令 Gas
基础指令 Gas 参数在 instr.rs 中有完整定义,这里列出他们的主要信息:
无操作
参数 | 含义 |
---|---|
nop | 无操作 |
流程控制
参数 | 含义 |
---|---|
ret | 返回 |
abort | 退出 |
br_true | 执行条件为 true 的分支 |
br_false | 执行条件为 false 的分支 |
branch | 分支 |
栈(Stack)
参数 | 含义 |
---|---|
pop | 弹出 |
ld_u8 | Load a u8 |
ld_u64 | Load a u64 |
ld_u128 | Load a u128 |
ld_true | Load a true |
ld_false | Load a false |
ld_const_base | 加载 constant 的基础消费 |
ld_const_per_byte | 加载 constant 的每字节消费 |
本地作用域
参数 | 含义 |
---|---|
imm_borrow_loc | 不可变更的 borrow(权限) |
mut_borrow_loc | 可变更的 borrow |
imm_borrow_field | 不可变更的字段 borrow(权限) |
mut_borrow_field | 可变更的字段 borrow |
imm_borrow_field_generic | |
mut_borrow_field_generic | |
copy_loc_base | copy 的基础消费 |
copy_loc_per_abs_val_unit | |
move_loc_base | Move |
st_loc_base |
调用
参数 | 含义 |
---|---|
call_base | 函数调用的基础消费 |
call_per_arg | 函数调用的每参数消费 |
call_generic_base | |
call_generic_per_ty_arg | 每个类型参数消费 |
call_generic_per_arg |
结构体(Structs)
参数 | 含义 |
---|---|
pack_base | 打包结构体的基础消费 |
pack_per_field | 打包结构体的每字段消费 |
pack_generic_base | |
pack_generic_per_field | |
unpack_base | 解包结构体的基础消费 |
unpack_per_field | 解包结构体的每字段消费 |
unpack_generic_base | |
unpack_generic_per_field |
引用(References)
参数 | 含义 |
---|---|
read_ref_base | 读取引用的基础消费 |
read_ref_per_abs_val_unit | |
write_ref_base | 写入引用的基础消费 |
freeze_ref | 冻结引用 |
类型转换(Casting)
参数 | 含义 |
---|---|
cast_u8 | 转为 u8 |
cast_u64 | 转为 u64 |
cast_u128 | 转为 u128 |
代数运算(Arithmetic)
参数 | 含义 |
---|---|
add | 加 |
sub | 减 |
mul | 乘 |
mod_ | 模(取余) |
div | 除 |
位运算(Bitwise)
参数 | 含义 |
---|---|
bit_or | 按位或: | |
bit_and | 按位与: & |
xor | 异或: ^ |
shl | 左移: << |
shr | 右移: >> |
逻辑运算(Boolean)
参数 | 含义 |
---|---|
or | 或: || |
and | 与: && |
not | 非: ! |
比较运算(Comparison)
参数 | 含义 |
---|---|
lt | 小于: < |
gt | 大于: > |
le | 小于等于: <= |
ge | 大于等于: >= |
eq_base | 基础相等: == |
eq_per_abs_val_unit | (绝对值相等?) |
neq_base | 基础不等: != |
neq_per_abs_val_unit |
全局存储(Global storage)
参数 | 含义 |
---|---|
imm_borrow_global_base | 不可变更 borrow 的基础消费: borrow_global |
imm_borrow_global_generic_base | |
mut_borrow_global_base | 可变更 borrow 的基础消费: borrow_global_mut |
mut_borrow_global_generic_base | |
exists_base | 检查是否存在的基础消费: exists |
exists_generic_base | |
move_from_base | move from 基础消费: move_from |
move_from_generic_base | |
move_to_base | move to 基础消费: move_to |
move_to_generic_base |
向量运算(Vectors)
参数 | 含义 |
---|---|
vec_len_base | 向量长度 |
vec_imm_borrow_base | 不可变更地 borrow 一个元素 |
vec_mut_borrow_base | 可变更地 borrow 一个元素 |
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.rs, move_stdlib.rs,其他相关源文件在这里 aptos-gas/src/.
存储 gas
存储 Gas 参数定义在 storage_gas.move 中, 里面还包含了一个完整的 DocGen 文件:storage_gas.md.
简单说:
- 在初始化函数中(initialize()),有一个 base_8192_exponential_curve() 方法,用于生成一条指数曲线,当存储利用率接近上限时,每元素和每字节的成本将快速上升。
- 在每个世代中,都可以通过 on_reconfig() 来配置基于元素或字节利用率的参数。
这些参数存储在 StorageGas 中,共包含下列字段:
字段 含义 per_item_read 从全局存储读取一个元素的消费 per_item_create 在全局存储创建一个元素的消费 per_item_write 向全局存储写入一个元素的消费 per_byte_read 从全局存储读取一个字节的消费 per_byte_create 在全局存储创建一个字节的消费 per_byte_write 向全局存储写入一个字节的消费
这里,所谓一个元素,或者是一个带 key 属性的资源,或者 table 中的一条入口记录;而 per-byte 类型的消费量,是根据元素的整体大小来评估的。例如在storage_gas.md 的描述里,如果一个操作修改了某项资源中的 u8 类型字段,而该资源还有5 个 u128 类型的资源,那么per-byte gas 的计算公式为: (5 * 128) / 8 + 1 = 81 bytes
.
向量
向量的按字节付费计算是类似的:
其中:
- nn 是向量中元素的格式
- ei 是元素 i 的容量
- b(n) 是函数 n 的基础容量
关于向量基础容量(技术上是 ULEB128)的更多信息,可以查看文档 BCS sequence specification,它在实践中通常只占一个字节,所以,含100 个 u8 类型元素的向量,占用 100 + 1 = 101个字节。 因此,根据上述的逐项读取方法,读取这样一个向量的最后一个元素被视为一个101字节的读取。
载荷 gas
载荷 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 数” 转换为 “外部 gas 数” 需要除以系数 gas_unit_scaling_factor。要把“外部 gas 数”转为 octas 为单位的货币,再乘以 "gas price",也就是每单位外部 gas 的价格。
优化原则
单位与价格常量
在本文档编写的时候,每 gas 单位最小价格(min_price_per_gas_unit)是定义在文件 transaction.rs 中的一个全局常量:aptos_global_constants::GAS_UNIT_PRICE (目前等于100)。该文件还定义了其他重要的常量:
常量 | 值 |
---|---|
min_price_per_gas_unit | 100 |
max_price_per_gas_unit | 10,000 |
gas_unit_scaling_factor | 10,000 |
Payload gas 一节详细解释了这些常量的含义
存储 gas
在本文编写的时候,初始化(initialize())方法设置了下列最低存储 gas 数量:
数据类型 | 操作 | 符号 | 最小内部 gas 数 |
---|---|---|---|
Per item | Read | r_iri | 300,000 |
Per item | Create | c_ici | 5,000,000 |
Per item | Write | w_iwi | 300,000 |
Per byte | Read | r_brb | 300 |
Per byte | Create | c_bcb | 5,000 |
Per byte | Write | w_bwb | 5,000 |
最大数量是最小数量的100倍,这意味着,当利用率不到40%,总的 gas 消费会在最小数量的 1 到 1.5 倍区间内(算法详见 base_8192_exponential_curve()。因此,以 octas 计价的话,主网络的初始 gas 消费如下表所示:(除以内部 gas 比例因子,再乘以最小 gas 价格):
操作 | 符号 | 最小 octas 花费 |
---|---|---|
Per-item read | r_iri | 3000 |
Per-item create | c_ici | 50,000 |
Per-item write | w_iwi | 3000 |
Per-byte read | r_brb | 3 |
Per-byte create | c_bcb | 50 |
Per-byte write | w_bwb | 50 |
我们可以看到,最昂贵的 per-item 模式,是创建一个新元素(通过 move_to
此外,
- per-item 模式中,读和写的花费是相等的:
- per-byte 模式中,写操作却跟创建操作一样贵:
- per-byte 模式中,写操作和创建操作的花费,几乎是读操作的 17 倍:
- Per-item读的花费是 per-byte 读的1000 倍:
- Per-item创建的花费是 per-byte 创建的1000 倍:
- Per-item写的花费是 per-byte 写的 60 倍:%22%20aria-hidden%3D%22true%22%3E%0A%20%3Cuse%20xlink%3Ahref%3D%22%23E1-MJMATHI-63%22%20x%3D%220%22%20y%3D%220%22%3E%3C%2Fuse%3E%0A%20%3Cuse%20transform%3D%22scale(0.707)%22%20xlink%3Ahref%3D%22%23E1-MJMATHI-69%22%20x%3D%22613%22%20y%3D%22-213%22%3E%3C%2Fuse%3E%0A%20%3Cuse%20xlink%3Ahref%3D%22%23E1-MJMAIN-3D%22%20x%3D%221055%22%20y%3D%220%22%3E%3C%2Fuse%3E%0A%3Cg%20transform%3D%22translate(2111%2C0)%22%3E%0A%20%3Cuse%20xlink%3Ahref%3D%22%23E1-MJMAIN-31%22%3E%3C%2Fuse%3E%0A%20%3Cuse%20xlink%3Ahref%3D%22%23E1-MJMAIN-30%22%20x%3D%22500%22%20y%3D%220%22%3E%3C%2Fuse%3E%0A%20%3Cuse%20xlink%3Ahref%3D%22%23E1-MJMAIN-30%22%20x%3D%221001%22%20y%3D%220%22%3E%3C%2Fuse%3E%0A%20%3Cuse%20xlink%3Ahref%3D%22%23E1-MJMAIN-30%22%20x%3D%221501%22%20y%3D%220%22%3E%3C%2Fuse%3E%0A%3C%2Fg%3E%0A%3Cg%20transform%3D%22translate(4113%2C0)%22%3E%0A%20%3Cuse%20xlink%3Ahref%3D%22%23E1-MJMATHI-63%22%20x%3D%220%22%20y%3D%220%22%3E%3C%2Fuse%3E%0A%20%3Cuse%20transform%3D%22scale(0.707)%22%20xlink%3Ahref%3D%22%23E1-MJMATHI-62%22%20x%3D%22613%22%20y%3D%22-213%22%3E%3C%2Fuse%3E%0A%3C%2Fg%3E%0A%3C%2Fg%3E%0A%3C%2Fsvg%3E#card=math&code=w_i%20%3D%2060%20w_b&id=zuuM5)
读和创建的 per-item 比 per-byte 是1000倍,而且写操作的 per-item 比 per-byte,只有60倍。
这样,由于缺乏对节省全局存储(比如 删除元素 move_from
- 尽量避免 per-item 模式的创建操作
- 保持对未使用元素的追踪,尽量覆写它们,而不是创建新的元素
- 尽量避免 per-item 模式的写操作
- 尽量多读少写
尽量减少所以操作中的字节数,特别是写操作
指令 gas
在本文编写的时候,所有的指令 gas 都乘以一个在 gas_meter.rs 中定义的常量 EXECUTION_GAS_MULTIPLIER ,目前是20。因此,以下有代表性的操作假设 Gas 成本如下:(内部 gas 除以比例系数,然后乘以最低 gas 价格):
操作 | 最小 octas 花费 |
---|---|
Table add/borrow/remove box | 240 |
Function call | 200 |
Load constant | 130 |
Globally borrow | 100 |
Read/write reference | 40 |
Load u128 on stack | 16 |
Table box operation per byte | 2 |
(注意 per-byte 模式的 table box 操作指令 gas,并不包含相应的存储 gas,它们是各自独立核算的)
作为比较,读取一个100字节的元素需要花费 个 octas,大约是函数调用的16.5倍,一般来说,指令 gas 成本主要由存储 gas 成本主导。
因此,值得注意的是,尽管从技术上讲,减少程序中的函数调用次数是有价值的,但工程实践中,我们几乎在所有优化中,都是致力于编写模块化、可分解的代码,并以减少存储 gas 成本为目标,而不是试图编写具有较少嵌套函数的重复代码块。
在极端情况下,指令 gas 当然是有可能大大超过存储 gas 成本的,比如实现一个10,000 次迭代的循环数学运算,但显然这种极端情况非常少见,大部分应用程序消耗的存储 gas 都远远大于指令 gas。
载荷 gas
截止本文编写的时刻,transaction.rs 中定义了一笔交易的最小内部 gas 成本为 1,500,000 内部单位(至少15,000 octas),如果载荷大于 600 字节,那么每字节增加 2,000 内部 gas 单位(最少20 octas)。交易允许的最大载荷为 65535 字节。因此在实践中,通常不怎么考虑载荷 gas 的成本。
我的语雀原文在这里