rust - 装箱类型时是否有最佳实践?

标签 rust heap-memory memcpy boxing stack-memory

在 C# 中,有结构和类。结构通常(即有异常(exception))分配在堆栈上,而类总是分配在堆上。因此,类实例对 GC 施加压力,被认为比结构“慢”。微软有 a best practice guide何时在类上使用结构。这表示在以下情况下考虑结构:

  • It logically represents a single value, similar to primitive types (int, double, etc.).
  • It has an instance size under 16 bytes.
  • It is immutable.
  • It will not have to be boxed frequently.

在 C# 中,使用大于 16 字节的结构实例通常被认为比垃圾收集类实例(动态分配)的性能更差。

就速度而言,盒装实例(堆分配)何时比非盒装等效实例(堆栈分配)表现更好?关于何时应该动态分配(在堆上)而不是坚持默认的堆栈分配,是否有任何最佳实践?

最佳答案

TL;DR:从无拳击开始,然后是侧写。


堆栈分配与盒装分配

这可能更明确:

  • 坚持堆栈,
  • 除非值(value)大到足以将其炸毁。

虽然在语义上编写fn foo() -> Bar 意味着将Bar 从被调用者框架移动到调用者框架,但实际上您是更有可能以等效于 fn foo(__result: mut * Bar) 签名的方式结束,其中调用者在其堆栈上分配空间并将指针传递给被调用者。

这可能并不总是足以避免复制,因为某些模式可能会阻止直接写入返回槽:

fn defeat_copy_elision() -> WithDrop {
    let one = side_effectful();
    if side_effectful_too() {
        one
    } else {
        side_effects_hurt()
    }
}

这里,没有魔法:

  • 如果编译器将返回槽用于one,那么如果分支评估为false,它必须将one移出然后实例化把新的WithDrop放进去,最后销毁一个
  • 如果编译器在当前堆栈上实例化one,并且必须返回它,那么它必须执行复制。

如果类型不需要Drop,就没有问题。

尽管存在这些奇怪的情况,但我建议尽可能坚持使用堆栈,除非分析显示有利于装箱的地方。


内联成员(member)或盒装成员(member)

这种情况要复杂得多:

  • struct/enum 的大小受到影响,因此 CPU 缓存行为受到影响:

    • 不太常用的大变体很适合装箱(或装箱的一部分),
    • 访问频率较低的大成员很适合进行装箱。
  • 同时还有装箱的成本:

    • 它与Copy 类型不兼容,并且隐式实现了Drop(如上所示,它禁用了一些优化),
    • 分配/释放内存具有无限延迟1,
    • 访问盒装内存会引入数据依赖性:在知道地址之前,您无法知道要请求哪个缓存行。

因此,这是一个非常好的平衡行为。对成员进行装箱或拆箱可能会提高代码库某些部分的性能,同时降低其他部分的性能。

绝对没有放之四海而皆准的办法。

因此,我再一次建议避免装箱,直到分析揭示一个对装箱有益的地方。

1 考虑在 Linux 上,进程中没有空闲内存的任何内存分配都可能需要系统调用,如果操作系统中没有空闲内存,系统调用可能会触发OOM killer 杀死一个进程,此时它的内存被回收并可用。一个简单的 malloc(1) 可能很容易需要 毫秒

关于rust - 装箱类型时是否有最佳实践?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45634972/

相关文章:

rust - 如何在不使用构建脚本的情况下将可执行文件的完整目标三重作为编译时常量?

rust - 调用一个借用为不可变的闭包时,不能在循环中借用为可变的吗?

enums - Rust 宏从另一个枚举生成枚举

Java 使用的堆与分配的对象大小

c - memcpy() 将整数值复制到字符缓冲区

rust - 如何从Rust中的函数返回计算出的字符串?

java - 内存中引用变量和对象的逻辑结构/详细信息?

c++ - 成员函数内存分配栈还是堆?

c++ - 无法将整数写入缓冲区内的偏移量 (char*)

c - C 中的原始数据类型,用于表示 CPU 架构的 WORD 大小