CLR(.NET)如何在内部分配和传递自定义值类型(结构)? 问题:

CLR(.NET)如何在内部分配和传递自定义值类型(结构)? 问题:,.net,garbage-collection,clr,memory-management,value-type,.net,Garbage Collection,Clr,Memory Management,Value Type,是否所有CLR值类型(包括用户定义的structs)都以独占方式存在于计算堆栈上,这意味着它们永远不需要由垃圾收集器回收,或者是否存在垃圾回收的情况 背景: 我以前问过一个问题。我特别担心创建大量非常短暂的临时对象会通过更频繁的垃圾收集对运行时性能产生负面影响 现在我突然想到,如果我将这些临时对象的类型声明为struct(即,作为用户定义的值类型)而不是class,那么如果所有值类型都独占地存在于计算堆栈上,那么垃圾收集器可能根本就不会涉及 (这主要是因为我想到了C++处理局部变量的方法。通常是

是否所有CLR值类型(包括用户定义的
struct
s)都以独占方式存在于计算堆栈上,这意味着它们永远不需要由垃圾收集器回收,或者是否存在垃圾回收的情况

背景: 我以前问过一个问题。我特别担心创建大量非常短暂的临时对象会通过更频繁的垃圾收集对运行时性能产生负面影响

现在我突然想到,如果我将这些临时对象的类型声明为
struct
(即,作为用户定义的值类型)而不是
class
,那么如果所有值类型都独占地存在于计算堆栈上,那么垃圾收集器可能根本就不会涉及

(这主要是因为我想到了C++处理局部变量的方法。通常是自动的(
auto
)变量,它们在堆栈上分配,因此在程序执行返回调用方时释放-根本不涉及通过
new
/
delete
进行动态内存管理。我认为CLR可能会处理类似的
struct
s。)

到目前为止,我发现: 我做了一个简短的实验,看看为用户定义的值类型和引用类型生成的CIL有什么不同。这是我的C#代码:

这是最后四行代码生成的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)
.init
(
[0]值类型某些值类型vt,
[1] 类SomeReferenceType rt,
[2] valuetype SomeValueType g_uuuinitLocal0//
[3] 类SomeReferenceType g\uu initLocal1,//为什么生成这些?
[4] valuetype某些valuetype CS$0$0000//
)
L_0000:ldloca.s$0$0000
L_0002:initobj SomeValueType//不需要newobj,实例已分配
L_0008:ldloc.s$0$0000
L_000a:stloc.2
L_000b:ldloca.sg_initLocal0
L_000d:ldc.i4.1
L_000e:stfld int32 SomeValueType::X
L_0013:ldloc.2
L_0014:stloc.0
L_0015:newobj实例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:调用无效程序::TryValueType(valuetype SomeValueType)
L_002a:ldloc.1
L_002b:调用void程序::TryReferenceType(类SomeReferenceType)
我无法从这段代码中看出:

  • .locals
    块中提到的所有局部变量都分配到哪里了?如何分配?他们是如何被释放的

  • (主题外:为什么需要这么多匿名局部变量并来回复制,只是为了初始化我的两个局部变量
    rt
    vt
    ?)


结构是值类型,用于局部变量时在堆栈上分配。但是,如果将局部变量强制转换为
对象
或接口,则该值将被装箱并在堆上分配

结果,结构在脱离作用域后被释放,此外,当不再有对对象的任何引用时,它们被装箱并移动到堆中,之后垃圾收集器负责释放它们


我不确定编译器生成所有局部变量的原因,但我假设使用它们是因为您使用了对象初始值设定项。首先使用编译器生成的局部变量初始化对象,并且仅在复制到局部变量的对象初始值设定项完全执行后初始化对象。这将确保您永远不会看到一个只有一些对象初始化器执行的实例。

< P>请考虑以下内容:

  • 值类型和引用类型之间的区别主要在于赋值语义。值类型是在赋值时复制的-对于
    结构,这意味着复制所有字段的内容。引用类型仅复制引用,而不复制数据。CLI规范对对象的分配位置没有任何承诺,依赖规范中没有的行为通常是一个危险的想法

  • 临时对象(局部对象)将存在于GC生成0中。GC已经足够聪明,可以在它们超出范围时(几乎)释放它们,或者在实际上最有效的时候释放它们。Gen0运行的频率足够高,您不需要切换到
    struct
    实例来高效管理临时对象

  • 可变值类型已经有导致bug的趋势,因为很难理解何时对副本和原始副本进行变异。正是出于这个原因,而指南是

  • g_initLocal0
    和相关字段存在,因为您使用的是对象初始值设定项。切换到参数化构造函数,您将看到它们消失
  • 值类型通常在堆栈上分配,引用类型通常在堆上分配,但这实际上不是.NET规范的一部分,也不能保证(在第一篇链接文章中,Eric甚至指出了一些明显的例外)

    更重要的是,假设堆栈通常比堆便宜就意味着任何使用堆栈语义的程序或算法都会比GC管理的堆运行得更快或更高效,这是完全错误的。而且GC堆完全有可能而且通常可能在拥有大量对象的情况下优于堆栈分配,因为现代GC实现实际上对不需要释放的对象的数量更加敏感(与完全固定在堆栈上的对象数量上的堆栈实现相反)

    换句话说,如果您已经分配了数千或数百万个临时对象,即使if您关于具有堆栈语义的值类型的假设在您的分词上也是正确的
    .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)
    
    Introducing *reference properties* on those value types, as would be the case with a fluent interface, further violates the [Principle of Least Surprise][3] by creating inconsistent semantics. The expectation for value types is that they are copied, *in their entirety*, on assignment, but when reference types are included among their properties, you will actually only be getting a shallow copy. In the worst case you have a mutable struct containing *mutable* reference types, and the consumer of such an object will likely erroneously assume that one instance can be mutated without affecting the other.
    
    There are always exceptions - [some of them in the framework itself][4] - but as a general rule of thumb, I would not recommend writing "optimized" code that (a) depends on private implementation details and (b) that you know will be difficult to maintain, *unless* you (a) have full control over the execution environment and (b) have actually profiled your code and verified that the optimization would make a significant difference in latency or throughput.