CLR中实际分配的局部变量在哪里?

时间:2017-11-27 22:07:41

标签: c# .net memory-management clr cil

我刚刚进入CLR和IL,我对此感到困惑。

我有以下C#代码:

int x = 1;
object obj = x;
int y = (int)obj;

IL拆解了这个

      // Code size       18 (0x12)
  .maxstack  1
  .locals init ([0] int32 x,
           [1] object obj,
           [2] int32 y)
  IL_0000:  nop
  IL_0001:  ldc.i4.1
  IL_0002:  stloc.0
  IL_0003:  ldloc.0
  IL_0004:  box        [mscorlib]System.Int32
  IL_0009:  stloc.1
  IL_000a:  ldloc.1
  IL_000b:  unbox.any  [mscorlib]System.Int32
  IL_0010:  stloc.2
  IL_0011:  ret

因此,ldloc.0指令"将索引0处的局部变量加载到堆栈上。"。但是当地人真正存储在哪里以及他们从何处装载。因为我认为有两个地方可以分配内存:线程堆栈和堆。变量应该存储在堆栈中。

现在,我想,堆栈只是一个"评估堆栈,而变量的内存分配是一个实现细节,依赖于平台和JIT编译器。实际上,我们可以将我们的程序使用的内存分成评估堆栈,托管堆和本地分配的内存。

这是真的吗?或者还有其他机制吗?

2 个答案:

答案 0 :(得分:7)

你正在严重混淆许多逻辑上不同的东西:

  • 仅仅因为变量是C#中的局部变量并不意味着它位于IL中的短期存储池中。 C#中的局部变量可以对应于相应IL中的短期存储,长期存储或评估堆栈。
  • IL中的短期存储和评估堆栈可以对应于jitted机器代码中的堆栈或寄存器存储。

在将C#编译为IL时,C#编译器使本地成为闭包类的成员 - 它们会进入长期存储池 - 当本地的生命周期可能更长时比激活方法。 (或者当方法的激活被分解成小块时,就像在异步方法中一样。)

如果当地人的生命周期很短,那么编译器的优化器会选择他们是在短期池还是在评估堆栈上;编译器称后者是“短暂的”本地人。决定何时将局部变成短暂的算法很有意思;有关详细信息,请参阅编译器源代码。

然后,抖动必须决定是将短期池变量和评估堆栈变量放入堆栈位置还是寄存器;它使用复杂的优化算法再次这样做,该算法根据寄存器的可用性等而变化。

最后,当然C#编译器和抖动都可以自由地将未读本地作为一个整体来实现;永远不会读取的存储空间无需实际分配。

答案 1 :(得分:4)

  

IL拆解了这个

这是未优化的代码,通常由调试版本生成。通常由发布版本生成的优化代码更像是:

        // Code size 10 (0xD)
.maxstack 1
IL_0000:  ldc.i4.1    
IL_0001:  box         [mscorlib]System.Int32
IL_0006:  unbox.any   [mscorlib]System.Int32
IL_000B:  pop         
IL_000C:  ret  

您的版本和我的版本之间最明显的两个区别是:

  1. 我没有nop指令。那"什么都不做"如果你在编译的C#的开头{上设置一个断点,那么指令在运行代码中没有任何用处,但它确实可以在IL中挂断点。
  2. 你们做了一些忙碌的工作,存储和加载我并不担心的变量副本。
  3. (这不是偶然的最佳版本。)

    重要的是要考虑到,不仅根据构建类型对C语言本地人对IL本地人的处理有所不同,但同样的事情也适用于jitting阶段。

    在涉及到以下内容时会有更大的差异:

    public static void Stuff()
    {
        int x = 2;
        Func<int> f = () => x * 2;
    }
    

    此处xf都是C#中的本地人,但在IL中,实际上是一个带有字段和方法的堆对象。

    本地根据上下文意味着不同的东西,无论是形容词还是名词。

    在C#中,local 表示方法中的方法参数和局部变量,包括名词和形容词。它们通常分配在&#34;堆栈&#34; (尽管在引用类型的情况下,堆栈分配的变量是指一个堆分配的对象),对于&#34;堆栈&#34;的几个含义。 (我们稍后会谈到)但并非总是如此(在yieldawait方法中捕获的本地人和当地人在电话之间保持[有时当他们不是时]是两个例子)。大多数时候我们不需要过多地考虑这个事实,但是我们做的几次可能会导致过分强调我们如何谈论它的概念。

    在IL local 中,名词是指在方法开始时初始化的一组强类型位置。作为形容词,它指的是那些 locals 以及我们推送和弹出的堆栈中的位置(在IL中我们需要考虑堆栈,< em>很多)。这些都是本地位置,因为它们可以被认为是&#34;靠近&#34;但是当我们谈论CIL时,其中只有一个通常被称为 locals 。 (如果我们更普遍地谈论理论,我们可能会把所有人都称为本地人,或者不是,这取决于我们在谈论理论的观点)。

      

    但是当地人真正存储在哪里以及他们从哪里装货。

    这取决于你真正的意思&#34;真的&#34;。但是考虑一下我们使用堆栈的原因是什么呢?它是实现调用方法的方法的一种方便(但不是唯一)的方法。您在堆栈上放置了一些值,以及有关您现在所处位置的信息,然后转到该方法。然后,当它完成时,你在堆栈上有任何返回值,可以做下一件事。

    IL参与者在参数之后和你正在使用的堆栈之前是一块空间,就像IL布局的方式所反映的那样;争论,本地人,推动和弹出。

    这就是事情在实际机器中的运作方式,因为我们可以在它不起作用时最容易看到:

    public static void Overflow()
    {
        Overflow();
        Overflow();
    }
    

    调用它,我们得到一个StackOverflowException(我有两次调用它,因为尾部调用优化不能将该异常变为永不返回,这几乎是可能的)。这意味着用作堆栈的实际实际内存块已全部用尽。

    实际堆栈和IL堆栈之间存在明确的关系并且因此方法参数,本地(在IL名词意义上)以及推送和弹出的值都可以与存储的值相关,这不足为奇。在那段记忆中。

    但它们也可以作为CPU中的寄存器实现,因此本地可能永远不会在内存中。

    他们甚至可能也不在那里。考虑我的代码的发布版本。实际上,让它成为一个完整的C#方法:

    public static void DoStuff()
    {
        int x = 1;
        object obj = x;
        int y = (int)obj;
    }
    

    现在让我们来称呼它:

    public static void CallDoStuff()
    {
        DoStuff();
    }
    

    因此,编译器已将DoStuff()转换为此答案顶部的代码,同时将CallDoStuff()转换为:

    call DoStuff
    ret
    

    我们运行我们的应用程序并首先调用CallDoStuff(),因此抖动必须编译它。它很可能会发现DoStuff()非常小(并且还有一些其他因素影响这个决定)根本不会产生函数调用,而是将所有这些指令内联到代码中。生成CallDoStuff。然后,它可能会看到未装箱的inty)未被使用,因此它可以将其保留,这意味着它可以将int装箱,这意味着它可以留下产生int,这意味着我们根本不需要xobjy的任何实际代码。

    在这种情况下,在这个级别上的答案是关于值&#34;真的&#34;是,是&#34;无处&#34;。