动态分配问题
原文:https://docs.elrond.com/developers/best-practices/the-dynamic-allocation-problem
### 避免内存分配
警告
智能合约必须避免动态分配。由于动态分配带来的性能损失,Elrond虚拟机配置了硬限制,并将阻止试图过多分配的合约。
这里有一些简单的指导方针,你可以用来确保你的合约有效执行。通过遵循它们,当你的合约被调用时,你可能会注意到燃气消耗的大量减少。编译后的 WASM 二进制文件也有可能变得更小,因此调用起来更快更便宜。
都是关于类型的
许多基本的 Rust 类型(如String
和Vec<T>
)是在堆上动态分配的。简单地说,这意味着程序(在这种情况下,智能合约)不断向运行时环境(VM)请求越来越多的内存。对于小的集合来说,这没有多大关系,但是对于大的集合来说,这可能会变得很慢,VM 甚至可能会停止合约,并将执行标记为失败。
主要问题是,基本的 Rust 类型非常渴望动态内存分配:他们要求比实际需要更多的内存。对于普通的程序来说,这对于性能来说是非常好的,但是对于智能合约来说,每条指令都要耗费大量的资源,这对成本甚至运行时故障都有很大的影响。
另一种方法是使用托管类型,而不是通常的 Rust 类型。所有托管类型,如BigUint
、ManagedBuffer
等。将它们的所有内容存储在虚拟机的内存中,而不是压缩内存,因此它们有很大的性能优势。但是您不需要关心内容“在哪里”,因为托管类型会在 VM 的帮助下自动跟踪内容。
托管类型的工作方式是只在约定内存中存储一个handle
,这是一个u32
索引,而实际的有效负载驻留在保留的 VM 内存中。因此,每当你必须添加两个BigUint
时,例如,你代码中的+
操作将只传递三个句柄:结果、第一个操作数和第二个操作数。通过这种方式,很少有数据被传递,这反过来使得一切都更便宜。由于这些类型只存储一个句柄,它们的内存分配大小是固定的,所以可以在堆栈上分配,而不是在堆上分配。
警告
如果您需要更新旧代码以利用托管类型,请花时间了解您需要做出的更改。这种更新很重要,不能自动完成。
基锈类型 vs 托管类型
下面是由Elrond框架提供的非托管类型(基本信任类型)及其托管对应类型的表格:
非托管(使用安全) | 非托管(在堆上分配) | 管理 |
---|---|---|
- | - | BigUint |
&[u8] |
- | &ManagedBuffer |
- | BoxedBytes |
ManagedBuffer |
ArrayVec<u8, CAP> 【1】 |
Vec<u8> |
ManagedBuffer |
- | String |
ManagedBuffer |
- | - | TokenIdentifier |
- | MultiValueVec |
MultiValueEncoded / MultiValueManagedVec |
ArrayVec<T, CAP> 【1】 |
Vec<T> |
ManagedVec<T> |
[T; N] 【2】 |
Box<[T; N]> |
ManagedByteArray<N> |
- | Address |
ManagedAddress |
- | H256 |
ManagedByteArray<32> |
- | - | EsdtTokenData |
- | - | EsdtTokenPayment |
在大多数情况下,托管类型可以作为基本 Rust 类型的替代。一个简单的例子,见 BigUint 操作。
我们还建议在需要连续的有用内存区域时,直接在堆栈(作为局部变量)上分配 Rust 数组。此外,避免为此目的分配可变的全局缓冲区,这需要使用unsafe
代码。
另外,考虑使用ArrayVec
,它提供了Vec
的功能,但是没有在堆上分配。相反,它需要直接在堆栈上分配一块内存,就像基本的 Rust 本地数组一样,但保留了Vec
的灵活性。
警告
确保在考虑部署到 mainnet 之前,您逐步迁移到托管类型和彻底测试您的代码。
**##### 提示
您可以使用erdpy contract report
命令来验证您的合约是否仍然需要动态分配。
**