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

Is there a best practice when a type should be boxed?

在 C# 中,有结构和 classes。结构通常(即有例外)堆栈分配,classes 总是堆分配。因此,Class 个实例对 GC 施加压力,并且被认为 "slower" 而不是结构。 Microsoft 有 a best practice guide 何时使用超过 class 的结构。这表示考虑一个结构 if:

  • 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 字节的结构实例的性能比垃圾收集的 class 个实例(动态分配)更差。

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

TL;DR:从无拳击开始,然后是个人资料。


堆栈分配与盒装分配

这可能更明确:

  • 坚持堆叠,
  • 除非这个值大到足以炸毁它。

虽然语义上fn foo() -> Bar意味着将Bar从被调用者框架移动到调用者框架,但实际上你更有可能以相当于 fn foo(__result: mut * Bar) 签名,其中调用者在其堆栈上分配 space 并将指针传递给被调用者。

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

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

这里,没有魔法:

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

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

尽管有这些奇怪的情况,但我建议尽可能坚持使用堆栈,除非分析揭示了一个对 box 有益的地方。


在线会员或盒装会员

这个案例要复杂得多:

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

    • 不太常用的大变体是装箱(或装箱部分)的良好候选者,
    • 不经常访问的大会员是装箱的好人选。
  • 同时还有装箱费用:

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

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

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

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

1 考虑到 Linux,进程中没有空闲内存的任何内存分配都可能需要系统调用,这如果 OS 中没有空闲内存,则可能会触发 OOM 杀手杀死进程,此时其内存将被回收并可用。一个简单的 malloc(1) 可能很容易需要 毫秒 .