使用StorePacking
优化存储
位压缩是一个简单的概念:使用尽可能少的位数来存储数据。如果做得好,它可以显著减少需要存储的数据大小。这在智能合约中尤为重要,因为存储是昂贵的。
在编写Cairo智能合约时,优化存储使用以减少 gas 成本是非常重要的。事实上,与交易相关的大部分成本都与存储更新相关;而每个存储槽位写入都需要 gas 成本。 这意味着通过将多个值打包到较少的槽位中,你可以减少智能合约的用户所需的 gas 成本。
Cairo提供了 StorePacking
trait,以便将结构体字段打包到较少的存储槽位中。例如,考虑一个具有3个不同类型字段的 Sizes
结构体。总大小为8 + 32 + 64 = 104位。这比单个 u128
的128位要小。这意味着我们可以将这3个字段都打包到一个单独的 u128
变量中。由于一个存储槽位最多可以容纳251位,我们打包后的值只需要一个存储槽位,而不是3个。
#![allow(unused)] fn main() { use starknet::{StorePacking}; use integer::{u128_safe_divmod, u128_as_non_zero}; #[derive(Drop, Serde)] struct Sizes { tiny: u8, small: u32, medium: u64, } const TWO_POW_8: u128 = 0x100; const TWO_POW_40: u128 = 0x10000000000; const MASK_8: u128 = 0xff; const MASK_32: u128 = 0xffffffff; impl SizesStorePacking of StorePacking<Sizes, u128> { fn pack(value: Sizes) -> u128 { value.tiny.into() + (value.small.into() * TWO_POW_8) + (value.medium.into() * TWO_POW_40) } fn unpack(value: u128) -> Sizes { let tiny = value & MASK_8; let small = (value / TWO_POW_8) & MASK_32; let medium = (value / TWO_POW_40); Sizes { tiny: tiny.try_into().unwrap(), small: small.try_into().unwrap(), medium: medium.try_into().unwrap(), } } } #[starknet::contract] mod SizeFactory { use super::Sizes; use super::SizesStorePacking; //don't forget to import it! #[storage] struct Storage { remaining_sizes: Sizes } #[external(v0)] fn update_sizes(ref self: ContractState, sizes: Sizes) { // This will automatically pack the // struct into a single u128 self.remaining_sizes.write(sizes); } #[external(v0)] fn get_sizes(ref self: ContractState) -> Sizes { // this will automatically unpack the // packed-representation into the Sizes struct self.remaining_sizes.read() } } }
pack
函数通过位移和加法将所有三个字段合并为一个 u128
值。unpack
则将这一过程逆转,将原始字段提取回结构体中。
如果您对位运算不熟悉,这里将解释示例中执行的运算:
我们的目标是将 tiny
, small
, 还有 medium
字段打包成一个 u128
值。
首先,打包时:
tiny
是一个u8
,因此我们只需用.into()
将其直接转换为u128
。这会创建一个低 8 位设置为tiny
值的u128
值。small
是一个u32
,因此我们首先将它左移 8 位(向左添加 8 位值为 0 的比特),为tiny
占用的 8 位留出空间。然后,我们将tiny
添加到small
中,将它们合并成一个u128
值。现在,tiny
的值占 0-7 位,small
的值占 8-39 位。- 同样,
medium
是一个u64
,因此我们将其左移 40 (8 + 32) 位(TWO_POW_40
),为前面的字段腾出空间。这需要占用 40-103 位。
当解包时:
- 首先,我们通过与 8 个 1 的位掩码(
& MASK_8
)进行比特 AND(&)来提取tiny
。这样就分离出打包值的最低 8 位,也就是tiny
的值。 - 对于
small
,我们右移 8 位 (/TWO_POW_8
),使其与位掩码对齐,然后与 32 个 1 的位掩码进行位和运算。 - 对于
medium
,我们右移 40 位。由于它是最后打包的值,我们不需要应用位掩码,因为高位已经为 0。
这种技术可用于任何一组适合打包存储类型位大小的字段。例如,如果一个结构体有多个字段,其位大小加起来为 256 位,那么可以将它们打包成一个 u256
变量。如果字段的位数加起来是 512 位,则可以将它们打包到一个 u512
变量中,依此类推。你可以定义自己的结构和逻辑来打包和解包它们。
其余的工作由编译器自动完成 - 如果一个类型实现了 StorePacking
trait,那么编译器将知道它可以使用 Store
trait 的 StoreUsingPacking
实现,在写入之前进行打包,在从存储中读取后进行解包。
然而,一个重要的细节是,StorePacking::pack
输出的类型也必须实现 Store
,以使 StoreUsingPacking
正常工作。大多数情况下,我们希望打包到 felt252 或 u256 类型中 - 但是如果你想打包到自定义类型中,请确保该类型实现了 Store
trait。