sta*_*ica 6 .net clr garbage-collection memory-management value-type
是否所有CLR值类型(包括用户定义的struct
s)都独立地存在于评估堆栈中,这意味着它们永远不需要被垃圾收集器回收,或者是否存在垃圾收集的情况?
我之前已经问过一个关于流畅的接口对.NET应用程序的运行时性能的影响的问题.我特别担心创建大量非常短暂的临时对象会通过更频繁的垃圾收集对运行时性能产生负面影响.
现在我已经知道,如果我将这些临时对象的类型声明为struct
(即用户定义的值类型)而不是class
,如果事实证明所有值类型都只存在于垃圾收集器上,则可能根本不涉及垃圾收集器.评估堆栈.
(这对我来说主要是因为我在考虑C++处理局部变量的方式.通常是自动(auto
)变量,它们被分配在堆栈上,因此当程序执行回到调用者时释放 - 没有动态内存管理通过new
/ delete
参与其中.我认为CLR 可能会处理struct
类似的问题.)
我做了一个简短的实验,看看为用户定义的值类型和引用类型生成的CIL有什么不同.这是我的C#代码:
struct SomeValueType { public int X; }
class SomeReferenceType { public int X; }
.
.
static void TryValueType(SomeValueType vt) { ... }
static void TryReferenceType(SomeReferenceType rt) { ... }
.
.
var vt = new SomeValueType { X = 1 };
var rt = new SomeReferenceType { X = 2 };
TryValueType(vt);
TryReferenceType(rt);
Run Code Online (Sandbox Code Playgroud)
这是为最后四行代码生成的CIL:
.locals init
(
[0] valuetype SomeValueType vt,
[1] class SomeReferenceType rt,
[2] valuetype SomeValueType <>g__initLocal0, //
[3] class SomeReferenceType <>g__initLocal1, // why are these generated?
[4] valuetype SomeValueType CS$0$0000 //
)
L_0000: ldloca.s CS$0$0000
L_0002: initobj SomeValueType // no newobj required, instance already allocated
L_0008: ldloc.s CS$0$0000
L_000a: stloc.2
L_000b: ldloca.s <>g__initLocal0
L_000d: ldc.i4.1
L_000e: stfld int32 SomeValueType::X
L_0013: ldloc.2
L_0014: stloc.0
L_0015: newobj instance void SomeReferenceType::.ctor()
L_001a: stloc.3
L_001b: ldloc.3
L_001c: ldc.i4.2
L_001d: stfld int32 SomeReferenceType::X
L_0022: ldloc.3
L_0023: stloc.1
L_0024: ldloc.0
L_0025: call void Program::TryValueType(valuetype SomeValueType)
L_002a: ldloc.1
L_002b: call void Program::TryReferenceType(class SomeReferenceType)
Run Code Online (Sandbox Code Playgroud)
我从这段代码中无法弄清楚的是:
.locals
分配的块中提到的所有局部变量在哪里?他们是如何分配的?他们是如何被释放的?
(偏离主题:为什么需要这么多匿名局部变量并来回复制,只是为了初始化我的两个局部变量rt
而且vt
?)
Jon*_*rop 11
您接受的答案是错误的.
值类型和引用类型之间的区别主要是赋值语义之一.在赋值时复制值类型 - 对于结构,这意味着复制所有字段的内容.引用类型仅复制引用,而不复制数据.堆栈是一个实现细节.CLI规范不承诺分配对象的位置,并且依赖于规范中不存在的行为是一个坏主意.
值类型的特征在于它们的值传递语义,但这并不意味着它们实际上被生成的机器代码复制.
例如,对复数进行平方的函数可以接受两个浮点寄存器中的实部和虚部,并将其结果返回到两个浮点寄存器中.代码生成器优化了所有复制.
有几个人在下面的评论中解释了为什么这个答案是错误的,但是一些主持人删除了所有这些.
临时对象(本地人)将存在于GC生成0中.GC已经足够聪明,一旦超出范围就可以释放它们.您无需为此切换到struct实例.
这完全是胡说八道.GC仅查看运行时可用的信息,此时范围的所有概念都已消失.GC"一旦超出范围"就不会收集任何东西.GC将在无法访问后的某个时刻收集它.
可变值类型已经倾向于导致错误,因为当您将副本与原始副本进行变更时很难理解.但是在这些值类型上引入引用属性(如使用流畅的接口的情况)将会变得一团糟,因为看起来结构的某些部分正在被复制而其他部分则不会被复制(即嵌套属性参考属性).我不能强烈推荐这种做法,它可能导致长期的各种维护问题.
同样,这完全是胡说八道.在值类型中引用是没有错的.
现在,回答你的问题:
是否所有CLR值类型(包括用户定义的结构)都独立地存在于评估堆栈中,这意味着它们永远不需要被垃圾收集器回收,或者是否存在垃圾收集的情况?
值类型当然不会"仅仅依靠评估堆栈".首选是将它们存储在寄存器中.如有必要,它们将溢出到堆栈中.有时他们甚至在堆上装箱.
例如,如果你编写一个循环遍历数组元素的函数,那么int
循环变量(值类型)很可能完全存在于寄存器中,永远不会溢出到堆栈或写入堆中.这就是Eric Lippert(微软C#团队,他自己写的"我不知道关于.NET的GC的所有细节")的意思,当他写道,当抖动选择时,值类型可以溢出到堆栈没有注册价值".对于较大的值类型(例如System.Numerics.Complex
)也是如此,但较大值类型不适合寄存器的可能性较高.
值类型不在堆栈上的另一个重要示例是当您使用具有值类型元素的数组时.特别是,.NET Dictionary
集合使用结构数组,以便在内存中连续存储每个条目的键,值和哈希值.这大大提高了内存局部性,缓存效率,从而提高了性能.值类型(和具体化的泛型)是.NET在此哈希表基准测试中比Java快17倍的原因.
我做了一个简短的实验,看看CIL产生的差异是什么......
CIL是一种高级中间语言,因此,不会向您提供有关寄存器分配和溢出到堆栈的任何信息,甚至不能为您提供准确的拳击图片.但是,查看CIL可以让您了解前端C#或F#编译器如何将某些值类型转换为将异步和理解等更高级别的结构转换为CIL.
有关垃圾收集的更多信息,我强烈推荐垃圾收集手册和内存管理参考.如果您想深入了解VM中值类型的内部实现,那么我建议您阅读我自己的HLVM项目的源代码.在HLVM中,元组是值类型,您可以看到生成的汇编器以及它如何使用LLVM尽可能地将值类型的字段保存在寄存器中,并优化掉不必要的复制,仅在必要时溢出到堆栈.
请考虑以下事项:
值类型和引用类型之间的区别主要是赋值语义之一. 在赋值时复制值类型 - 对于a struct
,这意味着复制所有字段的内容.引用类型仅复制引用,而不复制数据. 堆栈是一个实现细节.CLI规范不承诺分配对象的位置,依赖于规范中不存在的行为通常是一个危险的想法.
临时对象(本地人)将存在于GC生成0中.GC已经足够聪明,一旦超出范围就会(几乎)释放它们 - 或者实际上最有效的时候.Gen0运行得足够频繁,您无需切换到struct
实例来有效管理临时对象.
可变值类型已经倾向于导致错误,因为当您将副本与原始副本进行变更时很难理解.许多语言设计者自己都建议在可能的情况下尽可能地使值类型变为不可变,并且该指南得到了本网站上许多顶级贡献者的响应.
引入这些值类型的引用属性(如流畅的接口的情况),通过创建不一致的语义进一步违反了最小惊喜原则.值类型的期望是,他们被复制,在整体上的分配,但是当引用类型包括它们的属性中,你实际上只得到一个浅拷贝.在最坏的情况下,您有一个包含可变引用类型的可变结构,并且此类对象的使用者可能会错误地假设一个实例可以在不影响另一个实例的情况下进行变异.
总有一些例外 - 其中一些在框架本身 - 但作为一般的经验法则,我不建议编写"优化"代码,(a)依赖于私人实施细节和(b)你知道将很难维护,除非您(a)完全控制执行环境并且(b)实际分析了您的代码并验证优化会在延迟或吞吐量方面产生显着差异.
本g_initLocal0
及相关领域在那里,因为你是使用对象初始化.切换到参数化构造函数,你会看到它们消失了.
值类型通常在堆栈上分配,引用类型通常在堆上分配,但实际上并不是.NET规范的一部分,并且不能保证(在第一个链接的帖子中,Eric甚至指出了一些明显的例外).
更重要的是,假设堆栈通常比堆更便宜自动意味着使用堆栈语义的任何程序或算法将比GC管理的堆运行得更快或更有效,这是完全错误的.还有一些论文写在这个主题,这是完全可能的,往往容易为GC堆有大量的对象跑赢堆栈分配,因为现代GC的实现实际上是向对象的数量更敏感并不需要释放(与完全固定到堆栈上的对象数量的堆栈实现相反).
换句话说,如果你已经分配的临时对象的成千上万-甚至,如果你对值类型假定有栈语义在特定的环境特定的平台上也是如此-利用它仍然可以让你的程序慢!
因此,我将回到我原来的建议:让GC完成它的工作,并且如果没有在所有可能的执行条件下进行全面的性能分析,不要认为你的实现可以胜过它.如果您从干净,可维护的代码开始,您可以随时优化; 但是如果你以可维护性为代价编写你认为是性能优化代码的东西,后来证明你的性能假设是错误的,那么你的项目成本在维护开销,缺陷数量等方面会大得多.
归档时间: |
|
查看次数: |
1895 次 |
最近记录: |