Optimizing Storage Costs
Bit-packing is a simple concept: use as few bits as possible to store a piece of data. When done well, it can significantly reduce the size of the data you need to store. This is especially important in smart contracts, where storage is expensive.
Ketika menulis kontrak pintar Cairo, penting untuk mengoptimalkan penggunaan penyimpanan guna mengurangi biaya gas. Memang, sebagian besar biaya yang terkait dengan suatu transaksi berhubungan dengan pembaruan penyimpanan; dan setiap slot penyimpanan memerlukan biaya gas untuk ditulis. Ini berarti dengan memadatkan beberapa nilai ke dalam slot yang lebih sedikit, Anda dapat mengurangi biaya gas yang ditanggung oleh pengguna kontrak pintar Anda.
Integer Structure and Bitwise Operators
An integer is coded on a certain number of bits, depending on its size (For example, a u8
integer is coded on 8 bits).

Intuitively, several integers can be combined into a single integer if the size of this single integer is greater than or equal to the sum of the sizes of the integers (For example, two u8
and one u16
in one u32
).
But, to do that, we need some bitwise operators:
- multiplying or dividing an integer by a power of 2 shifts the integer value to the left or to the right respectively

- applying a mask (
AND
operator) on an integer value isolates some bits of this integer

- adding (
OR
operator) two integers will combine both values into a single one.

With these bitwise operators, let's see how to combine two u8
integers into a single u16
integer (called packing
) and reversely (called unpacking
) in the following example:

Bit-packing in Cairo
The storage of a Starknet smart contract is a map with 2251 slots, where each slot is a felt252
which is initialized to 0.
As we saw earlier, to reduce gas costs due to storage updates, we have to use as few bits as possible, so we have to organize stored variables by packing them.
For example, consider the following Sizes
struct with 3 fields of different types: one u8
, one u32
and one u64
. The total size is 8 + 32 + 64 = 104 bits. This is less than a slot size (i.e 251 bits) so we can pack them together to be stored into a single slot.
Note that, as it also fits in a u128
, it's a good practice to use the smallest type to pack all your variables, so here a u128
should be used.
struct Sizes {
tiny: u8,
small: u32,
medium: u64,
}
To pack these 3 variables into a u128
we have to successively shift them to the left, and finally sum them.

To unpack these 3 variables from a u128
we have to successively shift them to the right and use a mask to isolate them.

The StorePacking
Trait
Cairo provides the StorePacking
trait to enable packing struct fields into fewer storage slots. StorePacking<T, PackedT>
is a generic trait taking the type you want to pack (T
) and the destination type (PackedT
) as parameters. It provides two functions to implement: pack
and unpack
.
Here is the implementation of the example of the previous chapter:
use core::starknet::storage_access::StorePacking;
#[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!
use core::starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
remaining_sizes: Sizes,
}
#[abi(embed_v0)]
fn update_sizes(ref self: ContractState, sizes: Sizes) {
// This will automatically pack the
// struct into a single u128
self.remaining_sizes.write(sizes);
}
#[abi(embed_v0)]
fn get_sizes(ref self: ContractState) -> Sizes {
// this will automatically unpack the
// packed-representation into the Sizes struct
self.remaining_sizes.read()
}
}
In this code snippet, you see that:
TWO_POW_8
andTWO_POW_40
are used to shift left in thepack
function and shift right in theunpack
function,MASK_8
andMASK_32
are used to isolate a variable in theunpack
function,- all the variables from the storage are converted to
u128
to be able to use bitwise operators.
Teknik ini dapat digunakan untuk setiap kelompok bidang yang cocok dalam ukuran bit dari tipe penyimpanan yang dipadatkan. Sebagai contoh, jika Anda memiliki struktur dengan beberapa bidang yang ukuran bit-nya ditambahkan menjadi 256 bit, Anda dapat memadatkannya ke dalam satu variabel u256
. Jika ukuran bit-nya ditambahkan menjadi 512 bit, Anda dapat memadatkannya ke dalam satu variabel u512
, dan seterusnya. Anda dapat mendefinisikan struktur dan logika sendiri untuk memadatkan dan membuka padatan tersebut.
Sisa pekerjaan tersebut dilakukan secara ajaib oleh kompiler - jika suatu tipe mengimplementasikan trait StorePacking
, maka kompiler akan tahu bahwa dapat menggunakan implementasi StoreUsingPacking
dari trait Store
untuk mengemas sebelum menulis dan membongkar setelah membaca dari penyimpanan. Namun, satu detail penting adalah bahwa tipe yang dihasilkan oleh StorePacking::pack
juga harus mengimplementasikan Store
agar StoreUsingPacking
dapat berfungsi. Sebagian besar waktu, kita akan menginginkan pengemasan ke felt252 atau u256 - tetapi jika Anda ingin mengemas ke tipe milik Anda sendiri, pastikan bahwa tipe tersebut mengimplementasikan trait Store
.