代码之家  ›  专栏  ›  技术社区  ›  stakx - no longer contributing Saravana Kumar

clr(.net)如何在内部分配和传递自定义值类型(结构)?

  •  6
  • stakx - no longer contributing Saravana Kumar  · 技术社区  · 14 年前

    问题:

    执行所有clr值类型,包括用户定义的 struct S,只存在于评估堆栈上,这意味着它们将不需要由垃圾收集器回收,或者是否存在垃圾收集的情况?

    背景:

    我以前问过 question on SO about the impact that a fluent interface has on the runtime performance of a .NET application . 我特别担心,通过更频繁的垃圾收集,创建大量非常短的临时对象会对运行时性能产生负面影响。

    现在我发现,如果我将这些临时对象的类型声明为 结构 (即用户定义的值类型)而不是 class 如果结果显示所有值类型都只存在于评估堆栈上,则垃圾收集器可能根本不参与其中。

    这主要是因为我在思考C++处理局部变量的方法。通常是自动的( auto )变量,它们被分配到堆栈上,因此当程序执行返回到调用方时释放—没有通过 new / delete 完全参与其中。我以为CLR只是 可以 手柄 结构 同样)

    到目前为止我所发现的:

    我做了一个简短的实验,看看为用户定义的值类型和引用类型生成的CIL有什么区别。这是我的密码:

    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);
    

    这是为最后四行代码生成的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)
    

    我无法从这段代码中看出的是:

    • 其中提到的所有局部变量 .locals 是否分配了块?它们是如何分配的?他们是怎么被释放的?

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

    4 回复  |  直到 9 年前
        1
  •  11
  •   J D    12 年前

    你接受的答案是错误的。

    值类型和引用类型之间的区别主要是赋值语义之一。赋值时复制值类型-对于结构,这意味着复制所有字段的内容。引用类型只复制引用,而不复制数据。堆栈是一个实现细节。cli规范不承诺在哪里分配对象,而依赖规范中没有的行为是一个坏主意。

    值类型的特征是它们的传递值语义,但这并不意味着它们实际上是由生成的机器代码复制的。

    例如,对复数进行平方的函数可以接受两个浮点寄存器中的实部和虚部分量,并将其结果返回到两个浮点寄存器中。代码生成器优化了所有的复制。

    有几个人在下面的评论中解释了为什么这个答案是错误的,但一些主持人删除了所有答案。

    临时对象(局部变量)将生活在GC第0代中。GC已经足够聪明,一旦超出范围就可以释放它们。您不需要为此切换到结构实例。

    这完全是胡说八道。GC只看到运行时可用的信息,到那时,范围的所有概念都消失了。“一旦超出范围”,GC将不会收集任何东西。GC将在无法访问之后的某个时间点收集它。

    可变值类型已经有导致错误的倾向,因为当您改变一个副本与原始副本时,很难理解这一点。但是在这些值类型上引入引用属性,就像在一个流畅的接口上引入引用属性一样,将是一团糟,因为看起来结构的某些部分正在被复制,而其他部分没有复制(即引用属性的嵌套属性)。我不能强烈反对这种做法,它很容易导致各种长期维护头痛。

    再说一次,这完全是胡说八道。在值类型中具有引用没有任何错误。

    现在,要回答您的问题:

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

    值类型当然不会“只存在于评估堆栈上”。首选是将它们存储在寄存器中。如有必要,它们将溢出到堆栈中。有时它们甚至被放在一堆箱子里。

    例如,如果编写一个循环数组元素的函数,那么 int 循环变量(值类型)将完全存在于寄存器中,永远不会溢出堆栈或写入堆中。这就是埃里克·利珀特(微软C团队的成员)写的 "I don’t know all the details" 关于.NET的gc)意味着当他写下值类型时,当 "the jitter chooses to not enregister the value" . 对于较大的值类型(如 System.Numerics.Complex )但较大的值类型不适合寄存器的可能性更大。

    另一个不存在于堆栈中的值类型的重要示例是,当使用具有值类型元素的数组时。特别地, the .NET Dictionary collection uses an array of structs 为了在内存中连续存储每个条目的键、值和哈希。这显著提高了内存位置、缓存效率,从而提高了性能。值类型(和泛化泛型)是.NET比Java快17倍的原因。 this hash table benchmark .

    我做了一个简短的实验,看看CIL产生的差异是什么…

    CIL是一种高级的中间语言,因此,它不会向您提供关于寄存器分配和溢出到堆栈的任何信息,甚至不会向您提供装箱的准确图片。然而,查看CIL可以让您了解前端C或F编译器如何将一些值类型框起来,因为它可以将更高级的构造(如异步和理解)转换为CIL。

    关于垃圾收集的更多信息,我强烈建议 The Garbage Collection Handbook The Memory Managment Reference . 如果您希望深入了解vms中值类型的内部实现,那么我建议您阅读自己的源代码。 HLVM project . 在HLVM中,元组是值类型,您可以看到所生成的汇编程序,以及它如何使用LLVM尽可能将值类型的字段保存在寄存器中,并优化消除不必要的复制,仅在必要时溢出到堆栈。

        2
  •  5
  •   Community CDub    7 年前

    请考虑以下事项:

    1. 值类型和引用类型之间的区别主要是 赋值语义。 赋值时复制值类型-用于 struct ,这意味着复制所有字段的内容。引用类型只复制引用,而不复制数据。 The stack is an implementation detail . cli规范不承诺在哪里分配对象,通常依赖规范中没有的行为是一个危险的想法。

    2. 临时对象(局部变量)将生活在GC第0代中。GC已经足够聪明,可以在它们超出范围时(几乎)立即释放它们——或者在实际上最有效的时候释放它们。gen0运行频率足够高,不需要切换到 结构 用于有效管理临时对象的实例。

    3. 可变值类型已经有导致错误的倾向,因为当您改变一个副本与原始副本时,很难理解这一点。 Many of the language designers themselves recommend making value types immutable whenever possible 正是因为这个原因,指导方针是 echoed by many of the top contributors on this site .

      介绍 引用属性 在这些值类型上,与使用Fluent接口的情况一样,进一步违反了 Principle of Least Surprise 通过创建不一致的语义。对值类型的期望是它们被复制, 全部 ,但当引用类型包含在它们的属性中时,实际上只会得到一个浅副本。在最坏的情况下,您有一个包含 易变的 引用类型和此类对象的使用者可能会错误地假定一个实例可以在不影响另一个实例的情况下发生变化。

      总有例外- some of them in the framework itself -但作为一般经验法则,我不建议编写(a)依赖于私有实现细节和(b)您知道难以维护的“优化”代码, 除非 您(a)对执行环境具有完全控制权,(b)已经对代码进行了实际的概要分析,并验证了优化将在延迟或吞吐量方面产生显著的差异。

    4. 这个 g_initLocal0 和相关的字段在那里,因为您正在使用对象初始值设定项。切换到参数化的构造器,您将看到这些构造器消失。

    值类型是 典型地 在堆栈上分配,引用类型为 典型地 在堆上分配,但这实际上不是.NET规范的一部分,并且不能保证(在第一个链接的日志中,Eric甚至指出了一些明显的异常)。

    更重要的是,假设堆栈通常比堆便宜是不正确的 自动地 意味着任何使用堆栈语义的程序或算法都将比GC管理的堆运行得更快或更高效。 There a number of papers written on this topic 而且,对于一个GC堆来说,它完全有可能并且经常比具有大量对象的堆栈分配更出色,因为现代GC实现实际上对 不要 需要释放(而不是完全固定到堆栈上对象数量的堆栈实现)。

    换句话说,如果您已经分配了数千或数百万个临时对象-甚至 如果 关于具有堆栈语义的值类型的假设在特定环境中的特定平台上是正确的-利用它 仍然会使程序变慢!

    因此,我将返回到我最初的建议:让GC完成它的工作,并且不要假定在所有可能的执行条件下,如果没有完整的性能分析,您的实现就可以胜过它。如果您从干净的、可维护的代码开始,您可以在以后进行优化;但是如果您以可维护性为代价编写您认为是性能优化的代码,并且在以后的性能假设中被证明是错误的,那么您的项目在维护开销、缺陷计数等方面的成本会大得多。

        3
  •  4
  •   Hans Passant    14 年前

    它是一个JIT编译器实现细节,用于分配.locals。现在,我不知道有什么东西不能在堆栈帧上分配它们。通过调整堆栈指针来“分配”它们,通过重新设置堆栈指针来“释放”它们。很快,很难改进。但谁知道呢,20年后,我们可能都在运行带有CPU核心的机器,这些核心经过优化,只运行带有完全不同的内部实现的托管代码。JIT优化器可能以大量寄存器为核心,现在已经使用寄存器来存储局部变量。

    临时性由C编译器发出,以在对象初始值设定项引发异常时提供一些最低一致性保证。它防止代码在catch或finally块中看到部分初始化的对象。也用于using和lock语句中,如果您替换代码中的对象引用,它可以防止错误的对象被释放或解锁。

        4
  •  1
  •   Daniel Brückner    14 年前

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

    结果,结构在超出作用域后被释放,此外,它们被装箱并移动到堆中,之后垃圾收集器将负责在不再引用对象时释放它们。

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