Aptos 链上的交易,在不考虑市场供需的情况下,会收取一笔“基础 Gas 费”。它是由以下3部分组成:

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

一笔交易含有越多的函数调用,分支判断之类的复杂逻辑,就消耗越多的“指令” Gas 。相应地,如果交易中有越多的读写请求,就消耗越多的“存储” Gas。而一笔交易上附带的载荷(payload)越长(字节数多),就消耗越多的“载荷” Gas。
优化原则 一节所描述的,在基础 Gas 费中,存储 Gas 占用的比重最大。

指令 Gas

基础指令 Gas 参数在 instr.rs 中有完整定义,这里列出他们的主要信息:

无操作

参数含义
nop无操作

流程控制

参数含义
ret返回
abort退出
br_true执行条件为 true 的分支
br_false执行条件为 false 的分支
branch分支

栈(Stack)

参数含义
pop弹出
ld_u8Load a u8
ld_u64Load a u64
ld_u128Load a u128
ld_trueLoad a true
ld_falseLoad 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_basecopy 的基础消费
copy_loc_per_abs_val_unit
move_loc_baseMove
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<T>()
imm_borrow_global_generic_base
mut_borrow_global_base可变更 borrow 的基础消费: borrow_global_mut<T>()
mut_borrow_global_generic_base
exists_base检查是否存在的基础消费: exists<T>()
exists_generic_base
move_from_basemove from 基础消费: move_from<T>()
move_from_generic_base
move_to_basemove to 基础消费: move_to<T>()
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.
简单说:

  1. 在初始化函数中(initialize()),有一个 base_8192_exponential_curve() 方法,用于生成一条指数曲线,当存储利用率接近上限时,每元素和每字节的成本将快速上升。
  2. 在每个世代中,都可以通过 on_reconfig() 来配置基于元素或字节利用率的参数。
  3. 这些参数存储在 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.rsstorage_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_unit100
max_price_per_gas_unit10,000
gas_unit_scaling_factor10,000

Payload gas 一节详细解释了这些常量的含义

存储 gas

在本文编写的时候,初始化(initialize())方法设置了下列最低存储 gas 数量:

数据类型操作符号最小内部 gas 数
Per itemReadr_iri300,000
Per itemCreatec_ici5,000,000
Per itemWritew_iwi300,000
Per byteReadr_brb300
Per byteCreatec_bcb5,000
Per byteWritew_bwb5,000

最大数量是最小数量的100倍,这意味着,当利用率不到40%,总的 gas 消费会在最小数量的 1 到 1.5 倍区间内(算法详见 base_8192_exponential_curve()。因此,以 octas 计价的话,主网络的初始 gas 消费如下表所示:(除以内部 gas 比例因子,再乘以最小 gas 价格):

操作符号最小 octas 花费
Per-item readr_iri3000
Per-item createc_ici50,000
Per-item writew_iwi3000
Per-byte readr_brb3
Per-byte createc_bcb50
Per-byte writew_bwb50

我们可以看到,最昂贵的 per-item 模式,是创建一个新元素(通过 move_to<T>() 方法,或者增加一行元素到 table),几乎比读写一个已有元素贵了17倍:

此外,

  • 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<T>(),或者删除行 removing from a table)的激励措施,目前有效的存储 gas 优化策略如下:

  1. 尽量避免 per-item 模式的创建操作
  2. 保持对未使用元素的追踪,尽量覆写它们,而不是创建新的元素
  3. 尽量避免 per-item 模式的写操作
  4. 尽量多读少写
  5. 尽量减少所以操作中的字节数,特别是写操作

    指令 gas

    在本文编写的时候,所有的指令 gas 都乘以一个在 gas_meter.rs 中定义的常量 EXECUTION_GAS_MULTIPLIER ,目前是20。因此,以下有代表性的操作假设 Gas 成本如下:(内部 gas 除以比例系数,然后乘以最低 gas 价格):

操作最小 octas 花费
Table add/borrow/remove box240
Function call200
Load constant130
Globally borrow100
Read/write reference40
Load u128 on stack16
Table box operation per byte2

(注意 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 的成本。

我的语雀原文在这里


songofhawk
303 声望24 粉丝