装箱类型时是否有最佳实践?
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)
可能很容易需要 毫秒 .
在 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)
可能很容易需要 毫秒 .