Aptos概念——基础 Gas 费

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_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.
简单说:

  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_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() 方法,或者增加一行元素到 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(),或者删除行 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 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 的成本。

我的语雀原文在这里

你可能感兴趣的:(区块链区块链开发)