如果您去年一直活跃在编程社区中,那么您肯定听到过对 Rust 执行速度和性能以及Result
Rust 中出色类型的赞扬。
我可能应该提一下,我不是 Rust 开发人员。尽管如此,或者甚至可能正因为如此,我想知道如果 Rust 使用这个 Result 类型,它怎么会如此高效,因为就我而言,这个类型被实现为所谓的union
在C . 它在联合中包含一个错误和一个返回值,其中在给定时间只有一个有效。该类型还包含一个标志,指示结果是否包含错误或值。
如果我计数正确,并且假设错误存储为指针或引用(例如,在 64 位系统上占用内存中的 8 个字节),则联合最少 8 个字节 + 标志一个字节,使得9字节内存。
现在,通过填充,我假设在大多数系统上,这将被重新对齐以占用 12 个字节。相比之下,返回 int(32) 仅分配 4 个字节。因此,使用 Result 分配的内存应该是使用 int 的三倍。
这不是极大地浪费内存吗?我想象在循环中运行它,这会增加很多。
我不太明白为什么有人会声称 Rust 性能超级好,而 Result 却占用了那么多内存?
我知道有一些优化技巧可以减少内存使用量,例如使用带有选项的 NotZeroInt 使编译器可以使用零作为标志,从而避免为标志提供额外的字节。但对于大多数类型来说这并不适用,不是吗?
如果有人有进一步的见解,我很想听听。请注意,我不是 Rust 开发人员,出于好奇而提出这个问题,正如我在尝试移植此功能的库中观察到的那样,内存使用量急剧增加。
当然,RustResult<T, E>
和Option<T>
类型比某些移植库进行了更好的优化,但我无法想象这如何不会影响程序性能。
您似乎熟悉返回整数以报告退出状态的 API:零表示成功,非零表示失败。用惯用的 Rust 表达相同内容的 API 将实现相同或更好的性能1,同时更加类型安全。
第一步是使用
Result
将值分类为成功或失败。由于在成功的情况下我们不需要返回任何额外的内容,因此我们使用()
:这样,它确实比简单的使用更多的空间
i32
,因为至少需要一位信息来传达成功。尽管为了解决您的误解之一,但错误值没有指针间接寻址;它是在线存储的。因此,这将是 4 个字节用于i32
判别式 + 1 个字节用于判别 + 3 个字节的填充以实现 4 字节对齐 = 8 个字节。正如您所提到的,使用
NonZeroI32
是一个选项:无论如何,这可能会更清楚(
Err(0)
如果您习惯于用零表示成功,则接收可能会令人困惑)并提供Result
一位额外的信息,这样大小现在总共只有 4 个字节,因为编译器可以使用全零位- 用于NonZeroI32
表示成功的模式。然而,这仍然不是真正惯用的 Rust,因为它
NonZeroI32
并不是特别精致。通常,您会将错误类型定义为错误情况的枚举:现在,它更具表现力,因为它现在以值可以存在的类型进行编码,并且甚至更小,因为它的
MyError
变体少于 256 个,因此可以将其编码为带有位模式的单个字节以备用,这样整体Result
就是一个单字节。1. 小于机器字大小的值不太可能真正提高性能 - 如果可以与其他小值一起存储,则只能通过潜在地减少内存压力来提高性能。
另一种常见模式:有时错误类型可能会变得非常大(试图提供尽可能多的有用信息),但并不常见 - 或者至少不应该如此。在这种情况下,您可能希望为错误类型引入间接 via
Box
以减少堆栈大小。这将是机器字大小,因为
Box
保证不为空,因此全零位模式可以用于成功情况,就像 一样NonZeroI32
。您可能会发现图书馆可能已经在内部这样做了。例如,
Error
来自 serde-json 的内容如下所示:当然,在很多情况下,必须为判别式使用额外的空间 - 无论是由于成功类型、错误类型或两者兼而有之 - 但这只是表达您希望返回的数据的必要条件。即使需要额外的机器字,以这种方式处理错误通常比发生错误时抛出异常的成本更有效。
Rust 在如何分配枚举判别式方面比 C 的标记联合更加智能和优化,因为无法直接访问它们。此外,Rust 将泛型作为本机功能,这意味着 Result<T, E> 布局是单独定义的,并针对所使用的 T 和 E 的每个组合进行了优化。
由于泛型,无需使用引用,值直接包含在结果枚举的联合内。这意味着在大多数情况下,如果错误是错误代码,则最小错误 + 标志为 2 个字节。(一个用于错误判别式,另一个用于标志判别式)。
但是,您还需要能够返回一个值,该值是正常情况。该值通常有填充并且大于错误值。如果是这种情况,Rust 编译器能够对结果的鉴别器使用填充,从而产生一个与其最大值大小相同的标记枚举。这可以有效地释放 Result 对象的内存使用量。