使用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()
    }
}


}

通过实现 StorePacking trait来优化存储

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。

Last change: 2023-10-19, commit: 497bbcd