Skip to content
Document

Efficient gas practices

.

Minimizing gas usage is essential to ensure cost-effectiveness and optimal performance on the blockchain. In this article we have collected our best practice for handling gas optimization.

The size of the contract state directly affects the CPU cost, particularly during serialization and deserialization processes. As the contract state grows larger, both serialization and deserialization require more computational resources, resulting in increased gas costs. It is important to be aware of this impact and optimize gas usage accordingly when creating smart contracts.

Working with large states

When working with large states they can quickly grow to cost a lot of gas. Converting between data formats used by the blockchain and the formats used internally by the Rust contract can be slow, depending upon how the state is structured. To reduce the workload we should structure our states using formats that are easier to convert between. Notably, prefer using data-structures with fixed sizes (avoiding Option and Vec.) such that the blockchain can then understand and immediately serialize these by knowing the different lengths of the variables without looking at the actual data within. This makes it possible to serialize and deserialize with just one copy of the whole segment, thus only costing a fixed gas price.

In general it is recommended to always use a Vec<> with fix sized elements inside. If using a struct it's the same premise, when having a lot of fields, fix sized variables will save you the most amount of gas when used as part of either Vec maps or struct. We have created a table of all fix sized elements on PBC you can freely refer to as a guide when choosing what variable to use in your contracts.

A Vec containing thousands of addresses is more effective when taking the above points into account. You can dive into the technicalities of serialize and deserialize by visiting our Rust docs here.

Example 1 of optimizing a struct

For a struct to be serializable_by_copy, i.e. the method described above, the sizes of the struct's fields must sum to the size of the struct in memory, you can read more about it in the rust docs.

As an example of the above:

pub struct Unaligned {
    a: u8,    // offset 0, size 1
    b: u16,   // offset 2, size 2,
    c: u8,    // offset 4, size 1
    // total size: 5
}

Here the struct is unaligned because a is only 1 byte in memory and b is placed on 2 in memory. This creates an empty space in memory and therefore makes the struct Unaligned. To align the struct you need to switch a and b to use the offset efficiently, this creates the correct ratio of the fields within the struct to the sum of the structs fields placement in memory. See updated version below:

pub struct Aligned {
    b: u16,  // offset 0, size 2
    a: u8,   // offset 2, size 1
    c: u8,   // offset 3, size 1
    // total size: 4
}

Example 2 of optimizing a struct

To give another example we have created a struct looking like this:

pub struct Unaligned {
    to: Address,            // offset 0, size 21
    from: Address,          // offset 21, size 21
    amount: u128,           // offset 42, size 16 (Offset after here is 58).
    timestamp: i64,         // offset 64, size 8
    // size: 66 bytes using 72 bytes of memory
}

The struct has two address an amount and a timestamp. The problem here is that the sum of the struct is not total the sum of fields in memory, there are 6 unused bytes between amount and timestamp. To solve this issue we need to do two things. First we would need to introduce: #[repr(C)] to manage the exact order of memory ensuring that Rust does not reorder or try to fix the issue itself. Secondly we needed to introduce padding of 6 bytes padding: [u8; 6] to make sure that the struct has the correct ratio. The efficient struct can be seen below:

pub struct Aligned {
    to: Address,            // offset 0, size 21
    from: Address,          // offset 21, size 21
    padding: [u8; 6],       // offset 42, size 6
    amount: u128,           // offset 48, size 16
    timestamp: i64,         // offset 64, size 8
    // size: 72 bytes
}

In conclusion, efficient gas practices involve minimizing gas usage when working with large data, optimizing CPU costs based on contract state size and using fix sized elements with the right ratios. By incorporating these practices, developers can create smart contracts that are more cost-effective and performant on the blockchain.