Avoiding memory allocation
Smart contracts must avoid dynamic allocation. Due to the performance penalty incurred by dynamic allocation, the Elrond Virtual Machine is configured with hard limits and will stop a contract that attempts too much allocation.
Here are a few simple guidelines you can use to ensure your contract performs efficiently. By following them, you might notice a considerable reduction of gas consumption when your contract is called. It is also likely that the WASM binary resulting from compilation may become smaller in size, thus faster and cheaper to call overall.
It's all about the types
Many basic Rust types (like
Vec<T>) are dynamically allocated on the heap. In simple terms, it means the program (in this case, the smart contract) keeps asking for more and more memory from the runtime environment (the VM). For small collections, this doesn't matter much, but for bigger collection, this can become slow and the VM might even stop the contract and mark the execution as failed.
The main issue is that basic Rust types are quite eager with dynamic memory allocation: they ask for more memory than they actually need. For ordinary programs, this is great for performance, but for smart contracts, where every instruction costs gas, can be quite impactful, on both cost and even runtime failures.
The alternative is to use managed types instead of the usual Rust types. All managed types, like
ManagedBuffer etc. store all their contents inside the VM's memory, as opposed to the contract memory, so they have a great performance advantage. But you don't need to be concerned with "where" the contents are, because managed types automatically keep track of the contents with help from the VM.
The managed types work by only storing a
handle within the contract memory, which is a
u32 index, while the actual payload resides in reserved VM memory. So whenever you have to add two
BigUints for example, the
+ operation in your code will only pass the three handles: the result, the first operand, and the second operand. This way, there is very little data being passed around, which in turn makes everything cheaper. And since these types only store a handle, their memory allocation is fixed in size, so it can be allocated on the stack instead of having to be allocated on the heap.
If you need to update older code to take advantage of managed types, please take the time to understand the changes you need to make. Such an update is important and cannot be done automatically.
Base Rust types vs managed types
Below is a table of unmanaged types (basic Rust types) and their managed counterparts, provided by the Elrond framework:
|Unmanaged (safe to use)||Unmanaged (allocates on the heap)||Managed|
In most cases, the managed types can be used as drop-in replacements for the basic Rust types. For a simple example, see BigUint Operations.
We also recommend allocating Rust arrays directly on the stack (as local variables) whenever a contiguous area of useful memory is needed. Moreover, avoid allocating mutable global buffers for this purpose, which require
unsafe code to work with.
Also, consider using
ArrayVec, which provides the functionality of a
Vec, but without allocation on the heap. Instead, it requires allocation of a block of memory directly on the stack, like a basic Rust local array, but retains the flexibility of
Make sure you migrate to the managed types incrementally and thoroughly test your code before even considering deploying to the mainnet.
You can use the
erdpy contract report command to verify whether your contract still requires dynamic allocation or not.
ArrayVecallocates on the stack, and so it has a fixed capacity - it cannot grow indefinitely. You can make it as large as you please, but be warned that adding beyond this capacity results in a panic. Use
pushfor more graceful error handling. ↩ ↩
Be careful when passing arrays around, since they get copied when returned from functions. This can add a lot of expensive memory copies in your contract. ↩