【问题标题】:Is there a best practice when a type should be boxed?当一个类型应该被装箱时,是否有最佳实践?
【发布时间】:2018-01-19 22:59:25
【问题描述】:

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

  • 它在逻辑上表示单个值,类似于原始类型(int、double 等)。
  • 实例大小小于 16 字节。
  • 它是不可变的。
  • 不必经常装箱。

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

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

【问题讨论】:

    标签: rust heap-memory memcpy boxing stack-memory


    【解决方案1】:

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


    堆栈分配与盒装分配

    这可能更明确:

    • 坚持下去,
    • 除非价值大到足以将其炸毁。

    虽然语义写作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
    • 如果编译器在当前堆栈上实例化one,并且必须返回它,那么它必须执行复制。

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

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


    内联会员或盒装会员

    这个案例要复杂得多:

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

      • 不常用的大变体是装箱(或装箱部分)的良好候选者,
      • 访问频率较低的大成员是拳击的理想人选。
    • 同时,拳击也是有费用的:

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

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

    绝对没有一刀切的。

    因此,我再次建议避免装箱,直到剖析显示有利于装箱的地方。

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

    【讨论】:

      猜你喜欢
      • 2013-02-21
      • 2021-12-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多