Skip to main content

The dynamic allocation problem

Avoiding memory allocation

caution

Smart contracts must avoid dynamic allocation. Due to the performance penalty incurred by dynamic allocation, the MultiversX 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 String and 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 BigUint, 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.

caution

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 MultiversX framework:

Unmanaged (safe to use)Unmanaged (allocates on the heap)Managed
--BigUint
&[u8]-&ManagedBuffer
-BoxedBytesManagedBuffer
ArrayVec<u8, CAP>1Vec<u8>ManagedBuffer
-StringManagedBuffer
--TokenIdentifier
-MultiValueVecMultiValueEncoded / MultiValueManagedVec
ArrayVec<T, CAP>1Vec<T>ManagedVec<T>
[T; N]2Box<[T; N]>ManagedByteArray<N>
-AddressManagedAddress
-H256ManagedByteArray<32>
--EsdtTokenData
--EsdtTokenPayment

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 Vec.

caution

Make sure you migrate to the managed types incrementally and thoroughly test your code before even considering deploying to the mainnet.

tip

You can use the sc-meta report command to verify whether your contract still requires dynamic allocation or not.

Footnotes

  1. ArrayVec allocates 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 try_push instead of push for more graceful error handling. 2

  2. 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.