在 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/