6

这可能是一个非常简单的问题,但我在 SO 上找不到答案,也不知道有人问过答案:

我可以像这样编写一个简单的 c# 方法:

private void foo()
{
   int a = 1;
   int b = 5;
}

如果 CIL 代码(由编译器创建)由公共语言运行时执行,它将在堆栈顶部创建以下字段,而执行控件位于方法内部:

b = 5
a = 1

但是现在,我将方法扩展为访问名为“a”的字段:

private void foo()
{
   int a = 1;
   int b = 5;
   Console.WriteLine(a);
}

现在 CLR 必须访问一个不在堆栈顶部的字段,但根据 FILO(先进后出)原则,它必须在访问之前处理所请求字段之上的所有字段。

位于请求字段“a”上方的堆栈中名为“b”的字段会发生什么情况?

CLR不能删除它,因为它可能会被之后的执行方法使用,那么它会发生什么?

AFAIK,只有两种方法来存储字段,堆栈或堆。将它移到堆中并没有多大意义,因为这将利用 CLR 的堆栈带来的所有好处。CLR 是否会创建类似于第二个堆栈的东西?

这究竟是如何工作的?

-编辑-

也许我没有足够清楚地解释我的意图。

如果我写这样的方法:

private void foo()
{
   int a = 1;
   int b = 5;
   Console.WriteLine(a);
   Console.WriteLine(b);
}

CLR 首先将 2 个字段写入堆栈,然后再访问它们,但顺序相反。

首先,它必须访问字段“a”,但要访问它,CLR 必须处理位于堆栈上字段“a”之上的字段“b”。它不能只从堆栈中删除字段“b”,因为它必须在之后访问它。

这是如何运作的?

4

6 回答 6

7

变量不是单独堆叠的;堆栈包含“帧”。每个帧都包含当前方法调用所需的所有变量(局部变量、参数等)。因此,在您的示例中,ab在同一框架中彼此并存,无需删除它们中的任何一个。当方法foo完成时,整个堆栈帧从堆栈中弹出,将调用方法的帧留在顶部。

维基百科的文章可能会提供一些启示。

于 2012-07-31T13:44:11.927 回答
4

调用堆栈不是严格意义上的“纯”堆栈,您只能与顶部元素进行交互。在调用堆栈中,您正在堆叠整个函数调用和/或整个变量范围,而不是变量。

例如,如果foo()调用了一个新函数,比如说 ,它将把它的两个变量ab放在堆栈的顶部,并且可以完全访问它们。它(通常)不知道堆栈中这些变量下方的任何内容。

让我们看一下这段代码:

void foo() { // << Space is allocated on the stack for a and b.
             // << Anything in this scope has full access to a and b.
             // << But you cannot (normally) access anything from the
             // << calling function.
    var a = 1;
    var b = 2;

    if (a == 1) {  // << Another variable scope is placed on the stack.
                   // << From here you can access a, b and c.
        var c = 3;
    } // << c is removed from the stack.
} // << a, b and anything else in foo() is removed from the stack.
于 2012-07-31T13:44:47.520 回答
4

您对堆栈的印象是错误的,它只是在方法调用之间充当堆栈。在一个方法中,栈帧就像一个局部变量数组。托管代码的堆栈帧也没有什么特别之处,它的运行方式与本机 C 或 C++ 代码中使用的堆栈帧完全相同。

局部变量与 EBP 寄存器(堆栈帧指针)有一个固定的偏移量。该偏移量由 JIT 编译器确定。

您发布的代码的具体结果是内置在即时编译器中的优化器只会消除未使用的局部变量。最后一个示例中的a变量很可能最终会出现在 cpu 寄存器中,并且永远不会出现在堆栈中。标准优化。

于 2012-07-31T13:50:09.547 回答
3

请注意,当您谈论字段时,a它们b被称为局部变量

也许以下简化的逻辑表示可以澄清事情。在调用 之前Console.WriteLine,堆栈的顶部看起来像这样:

|5| // b
|1| // a

在内部Console.WriteLine,为其参数添加了一个额外的堆栈帧(称为value,它获取变量的副本a):

|1| // value = a
|5| // b
|1| // a

一旦 Console.WriteLine 返回,顶帧被弹出,堆栈再次变为:

|5| // b
|1| // a
于 2012-07-31T13:46:34.763 回答
1

对于 CLR,最好将局部变量视为编号的“槽”,例如邮箱。存储在这些“槽”中的值是否最终出现在方法的堆栈帧中(这里的其他人已经涵盖了这个概念),存储在 CPU 寄存器中,甚至完全优化出来都是抖动细节。有关详细信息,请参阅 IL Stloc指令。

最好考虑 CLR 运行一个执行堆栈,根据正在执行的指令弹出和推送值。托管代码如何在 CPU 上运行和执行的底层细节是另一回事,这就是传统的堆栈帧、寄存器和指针解引用重新发挥作用的地方。然而,从 IL 级别的 CLR 的角度来看,这些事情(大部分)是无关紧要的。

于 2012-07-31T13:53:22.707 回答
1

有四个相关但不同的概念:C# 中的局部变量、CIL 中的局部变量、CIL 中的堆栈和本机堆栈。

请注意,C# 局部变量如何映射到 CIL 以及 CIL 局部变量和堆栈如何映射到本机内存是实现定义的,因此您不应依赖任何这些。

你知道 C# locals 是什么。它们可以表示为 CIL 局部变量,但它们通常不会进入 CIL 堆栈(C# 编译器中可能有一些优化会这样做)。但是也有很少的其他选项:如果不需要,可以完全优化局部变量,或者可以将其编译为类中具有难以描述的名称的字段(lambda 的闭包变量,yield方法或async方法中的变量)。此外,即使某些 C# 局部变量被编译为 CIL 局部变量,它们也不必映射 1:1,因为如果编译器知道这样做是安全的,一个 CIL 局部变量可以用于更多 C# 局部变量。

在 CIL 中,有局部变量和堆栈。局部变量与堆栈完全分离,并且每个变量都有不同的 CIL 指令。局部变量用于保存更长时间需要的值,并且可以随时访问每个局部变量。CIL 堆栈主要包含目前正在使用的值:指令参数及其返回值。在堆栈中,只能访问顶部的值。

CIL 本地和 CIL 堆栈实际上都放置在本机堆栈上,但如果它们适合的话,它们通常只是在寄存器中。当然,JIT 编译器可以进行任何其他优化。正如其他人所说,当前方法堆栈中的任何值都可以随时访问,而不仅仅是顶部。

于 2012-08-02T08:52:15.260 回答