Hello everyone, In my latest work on a transaction logger canister—which extracts bridge transactions from various minters (including ckETH minter canister and couple of other Appic minters that connect evm chains to icp) — I faced challenges in optimizing cycle consumption. This logger canister makes 1-minute interval calls to different minters for scraping minter events, transforming the data, recording them into stable memory, and preparing it for user queries.
Initially, the canister consumed 200 billion cycles daily, mainly due to the heavy cost of state serialization and deserialization, which involved reading and writing to stable memory. Here’s how I solved the issue and made a drastic cut in consumed cycles.
The Problem
I was using ciborium
for serialization and deserialization(I’ve seen a lot of Dfinity canisters also use CBOR for this purpose). While functional, its performance in both encoding and decoding was too slow and heavy in used CPU and Memory, leading to high cycle consumption. Query responses for fetching transactions by address also took longer than desired.
The Solution
Switching to bincode for serialization and deserialization resulted in massive efficiency gains. Here’s a performance comparison:
---- state::tests::compare_bincode_and_ciborium stdout ----
Bincode - Serialization: 9.23µs, Deserialization: 15.318µs
Ciborium - Serialization: 10.127µs, Deserialization: 39.593µs
As you can see Deserialization is almost 2.5x faster.
Results
-
Cycle Consumption:
-
Dropped from 200 billion cycles/day to 40 billion cycles/day (a reduction of 80%).
-
Having said that this amount of cut in cycles consumption is not only achieved by state encoding and decoding changes, but also factors like(Avoiding unnecessary clones, better algorithms for mapping data and etc…
-
-
Query Performance:
- Transaction query responses are now 2x faster.
-
State Size:
- Stable memory size is roughly the same as using ciborium but slightly more memory which is not that significant .
---- state::tests::compare_bincode_and_ciborium_size stdout ----
Bincode - Size: 80 bytes
Ciborium - Size: 77 bytes
Why This Matters
Serialization and deserialization directly impact the efficiency of stable memory operations, especially in high-frequency canisters like mine. Optimizing these processes can drastically reduce costs and improve user experience.
If you’re building high-performance canisters, bincode might be worth considering for your serialization needs.
Drawbacks
Using bincode comes with its own hassles, for example you should define custom serializer and deserializer for types that are not supported by serde or bincode is native to rust only and if you need interoperability with other languages or tools its better to use cbor.
I would love to know your thoughts on this.