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

Kir*_*pak 4 .net c# clr cil memory-management

我只是进入CLR和IL,我对此感到困惑.

我有以下C#代码:

int x = 1;
object obj = x;
int y = (int)obj;
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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

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

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

Eri*_*ert 8

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

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

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

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

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

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


Jon*_*nna 5

而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  
Run Code Online (Sandbox Code Playgroud)

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

  1. 我的没有nop指令。该“不执行任何操作”指令在运行的代码中没有任何作用,但是,如果您在{从其编译的C#的开头放置了断点,则确实在IL上挂了一个断点。
  2. 您会做一些忙碌的工作,以存储和加载我不会打扰的变量副本。

(顺便说一下,这不是最佳的版本)。

重要的是要考虑到,不仅C#本地对象到IL本地对象的处理方式会根据构建类型而有所不同,而且同样的事情也适用于拼合阶段。

当涉及到以下内容时,甚至存在更大的差异:

public static void Stuff()
{
    int x = 2;
    Func<int> f = () => x * 2;
}
Run Code Online (Sandbox Code Playgroud)

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

根据上下文的不同,局部意味着不同的事物,既是形容词又是名词。

在C#中,local表示方法的参数和方法中的局部变量,既是名词又是形容词。它们通常在“堆栈”上分配(尽管在引用类型的情况下,堆栈分配的变量引用的是堆分配的对象)对于“堆栈”的几种含义(我们将在后面介绍),但并非总是如此(捕获的本地对象和在调用之间维护的yielding或awaiting方法中的本地对象(有时不是)是两个示例)。大多数时候,我们不需要对这个事实进行过多思考,但是,当我们这样做时,有几次往往会导致我们在谈论这个概念时过分强调这个概念。

在IL 本地作为名词是指一组在所述方法的开始初始化强类型位置。作为形容词,它既指那些本地对象,又指我们要推入和从中弹出的堆栈中的位置(在IL中,我们确实需要考虑很多堆栈)。这些都是本地位置,在很大程度上,它们可以被认为是“附近”,但是当我们谈论CIL时,通常只有其中一个被称为本地人。(如果我们更广泛地谈论理论,我们可能会把他们全部称为本地人,这取决于我们所谈论的理论的观点)。

但是当地人真正存储在哪里以及从何处加载。

这取决于“真正”的真正含义。但是请考虑我们使用堆栈的原因到底是什么。这是实现方法调用方法的便捷(但不是唯一)方法。您将一些值以及有关当前位置的信息放入堆栈中,然后移入该方法。然后,当它完成时,您在堆栈上就可以有任何返回值,并且可以做下一步。

IL局部变量在参数之后和要使用的堆栈块之前是一块空格,正如IL的布局方式所反映的那样;参数,本地变量,推送和弹出。

这也是事物在实际机器中的工作方式,我们很容易在不工作的情况下看到它:

public static void Overflow()
{
    Overflow();
    Overflow();
}
Run Code Online (Sandbox Code Playgroud)

调用它,我们得到一个StackOverflowException(我自己调用了两次,因为这样的话,尾部调用优化就无法将异常转化为永不返回,这几乎是可能的)。这意味着用作堆栈的内存的实际实际块已全部用完。

实际的堆栈和IL堆栈具有确定的关系也就不足为奇了,因此方法参数,局部变量(在IL名词意义上)以及压入和弹出的值都可以与存储在该内存块中的值相关。

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

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

public static void DoStuff()
{
    int x = 1;
    object obj = x;
    int y = (int)obj;
}
Run Code Online (Sandbox Code Playgroud)

现在让我们称之为:

public static void CallDoStuff()
{
    DoStuff();
}
Run Code Online (Sandbox Code Playgroud)

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

call DoStuff
ret
Run Code Online (Sandbox Code Playgroud)

我们运行我们的应用程序并到达CallDoStuff()第一个调用的位置,因此抖动必须对其进行编译。很可能会看到它DoStuff()很小,并且(连同其他一些影响该决策的因素)根本不会产生函数调用,而是将所有这些指令内联到为其生成的代码中CallDoStuff。然后,它可能会看到未使用unboxed inty),因此可以将其省略,这意味着可以将box intout 留出,这意味着可以保留产生intout,这意味着我们不需要任何实际的代码xobj以及y所有。

在这种情况下,关于“真正”值在哪里的答案是“无处”。