应该装箱型时是否有最佳做法?

Noe*_*mer 6 heap stack boxing memcpy rust

在C#中,有结构和类.结构通常(即有异常)堆栈分配,类总是堆分配.因此,类实例会对GC施加压力,并且被认为比结构"慢".Microsoft有一个最佳实践指南何时在类上使用结构.这表示在以下情况下考虑结构:

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

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

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

Mat*_* M. 6

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()
    }
}
Run Code Online (Sandbox Code Playgroud)

在这里,没有魔力:

  • 如果编译器使用返回槽one,那么在分支求值的情况下false它必须one移出然后将new实例化WithDrop到其中,最后销毁one,
  • 如果编译器one在当前堆栈上实例化,并且必须返回它,那么它必须执行复制.

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

尽管有这些古怪的情况下,我建议坚持堆栈如果可能的话,除非分析显示它会是有益的箱子的地方.


内联会员或盒装会员

这种情况要复杂得多:

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

    • 较少使用的大型变体是拳击(或拳击部分)的良好候选人,
    • 较少访问的大会员是拳击的好人选.
  • 与此同时,拳击有成本:

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

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

绝对没有任何一种尺寸适合所有人.

因此,我再一次建议避免拳击,直到剖析显示一个有利于盒子的地方.

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