堆栈的用途是什么?我们为什么需要它?

What is the purpose of a stack? Why do we need it?

提问人:Jan Carlo Viray 提问时间:10/24/2011 最后编辑:svickJan Carlo Viray 更新时间:6/6/2020 访问量:22436

问:

因此,我现在正在学习 MSIL,以学习调试我的 C# .NET 应用程序。

我一直在想:堆栈的目的是什么?

只是为了把我的问题放在上下文中:
为什么会有从内存到堆栈或“加载”的转移? 另一方面,为什么会有从堆栈到内存或“存储”的转移?为什么不把它们都放在内存中呢?

  • 是因为它更快吗?
  • 是因为它是基于RAM的吗?
  • 为了效率?

我正在努力掌握这一点,以帮助我更深入地理解 CIL 代码。

C# NET vb.net cil .net 程序集

评论

28赞 CodesInChaos 10/24/2011
堆栈是内存的一部分,就像堆是内存的另一部分一样。
0赞 Jan Carlo Viray 10/24/2011
您@CodeInChaos谈论值类型与引用类型?还是在IL代码方面也一样?...我知道堆栈比堆更快、更高效(但这是在 value/ref 类型世界中......我不知道这里是否相同?
15赞 Damien_The_Unbeliever 10/24/2011
@CodeInChaos - 我认为 Jan 引用的堆栈是写入 IL 的堆栈机器,而不是在函数调用期间接受堆栈帧的内存区域。它们是两个不同的堆栈,在 JIT 之后,IL 堆栈不存在(无论如何,在 x86 上)
4赞 Piotr Perak 10/26/2011
MSIL 知识将如何帮助您调试 .NET 应用程序?
1赞 Kuba hasn't forgotten Monica 3/19/2014
在现代机器上,代码的缓存行为是性能的创造者和破坏者。记忆无处不在。通常,堆栈就在这里。假设堆栈是一个真实的东西,而不仅仅是一个用于表达某些代码操作的概念。在实现运行 MSIL 的平台时,不要求堆栈概念必须让硬件实际推动位。

答:

20赞 user468687 10/24/2011 #1

有一篇关于此的非常有趣/详细的维基百科文章,堆栈机指令集的优点。我需要完全引用它,所以简单地放一个链接更容易。我只引用副标题

  • 非常紧凑的目标代码
  • 简单的编译器/简单的解释器
  • 最小处理器状态

评论

0赞 Tim Lloyd 10/24/2011
-1 @xanatos 你能试着总结一下你所采取的标题吗?
0赞 xanatos 10/24/2011
@chibacity 如果我想总结它们,我会做一个答案。我试图挽救一个非常好的链接。
0赞 Tim Lloyd 10/24/2011
@xanatos我理解你的目标,但分享这么大的维基百科文章的链接并不是一个好的答案。通过谷歌搜索并不难找到。另一方面,汉斯有一个很好的答案。
0赞 xanatos 10/24/2011
@chibacity OP 可能懒得先搜索。这里的回答者给出了一个很好的链接(没有描述)。两害一善:-)我会给汉斯投赞成票。
0赞 Jan Carlo Viray 10/24/2011
回答者并@xanatos +1 以获得很棒的链接。我一直在等待有人完全总结并给出知识包答案。如果汉斯没有给出答案,我会把你的答案作为公认的答案。只是这只是一个链接,所以对于汉斯来说,这不公平,他在他的答案上付出了很大的努力。:)
5赞 Azodious 10/24/2011 #2

如果不遵循堆栈/堆的概念,并且将数据加载到随机内存位置,或者数据从随机内存位置存储...这将是非常非结构化和不受管理的。

这些概念用于将数据存储在预定义的结构中,以提高性能、内存使用率......因此称为数据结构。

86赞 Hans Passant 10/24/2011 #3

请记住,当您谈论 MSIL 时,您谈论的是虚拟机的指令。.NET 中使用的 VM 是基于堆栈的虚拟机。与基于寄存器的 VM 相反,Android 操作系统中使用的 Dalvik VM 就是一个例子。

VM 中的堆栈是虚拟的,由解释器或实时编译器将 VM 指令转换为在处理器上运行的实际代码。在 .NET 的情况下,这几乎总是抖动,MSIL 指令集被设计为从一开始就被抖动。例如,与 Java 字节码相反,它对特定数据类型的操作有不同的指令。这使得它经过优化以进行解释。不过,MSIL 解释器实际上存在,它在 .NET Micro Framework 中使用。它在资源非常有限的处理器上运行,负担不起存储机器代码所需的 RAM。

实际的机器代码模型是混合的,既有堆栈又有寄存器。JIT代码优化器的一大工作是想出一种方法来存储保存在寄存器中的堆栈上的变量,从而大大提高执行速度。Dalvik 抖动则相反。

否则,机器堆栈是一个非常基本的存储设施,在处理器设计中已经存在了很长时间。它具有非常好的参考局部性,这是现代 CPU 的一个非常重要的功能,它咀嚼数据的速度比 RAM 提供数据的速度快得多,并且支持递归。语言设计在很大程度上受到堆栈的影响,堆栈在对局部变量的支持中可见,范围仅限于方法主体。堆栈的一个重大问题是该站点命名的那个问题。

评论

2赞 Jan Carlo Viray 10/24/2011
+1 用于非常详细的解释,+100(如果可以的话)用于与其他系统和语言:)的额外详细比较
4赞 Johannes Rudolph 10/28/2011
为什么 Dalvik 是 Register 机器?Sicne 它主要针对 ARM 处理器。现在,x86 具有相同数量的寄存器,但作为 CISC,其中只有 4 个真正可用于存储局部变量,因为其余的都隐式用于通用指令中。另一方面,ARM 架构有更多的寄存器可用于存储局部变量,因此它们促进了基于寄存器的执行模型。
1赞 Luaan 12/9/2016
@JohannesRudolph 近二十年来,情况并非如此。仅仅因为大多数 C++ 编译器仍然面向 90 年代的 x86 指令集并不意味着 x86 本身效率低下。例如,Haswell 有 168 个通用整数寄存器和 168 个 GP AVX 寄存器,远远超过我所知道的任何 ARM CPU。您可以根据需要以任何方式使用(现代)x86 程序集中的所有内容。责怪编译器编写者,而不是架构/CPU。事实上,这也是中间编译如此吸引人的原因之一——一个二进制的、给定 CPU 的最佳代码;没有 90 年代建筑的混乱。
2赞 Luaan 12/9/2016
@JohannesRudolph .NET JIT 编译器实际上非常大量地使用寄存器;堆栈主要是 IL 虚拟机的抽象,实际在 CPU 上运行的代码非常不同。方法调用可能是传递寄存器,局部变量可以提升到寄存器......机器代码中堆栈的主要好处是它为子例程调用提供了隔离 - 如果你在寄存器中放置一个本地,函数调用可能会使你失去该值,而你无法真正分辨。
1赞 Luaan 4/7/2018
@RahulAgarwal 生成的机器代码可能会也可能不会将堆栈用于任何给定的本地或中间值。在 IL 中,每个参数和局部都在堆栈上 - 但在机器代码中,这不是真的(这是允许的,但不是必需的)。有些东西在堆栈上是有用的,它们被放在堆栈上。有些东西在堆上很有用,它们被放在堆上。有些事情根本不需要,或者只需要在登记册中花几分钟时间。调用可以完全消除(内联),也可以在寄存器中传递其参数。JIT有很大的自由度。
448赞 Eric Lippert 10/24/2011 #4

更新:我非常喜欢这个问题,我在2011年11月18日将其作为博客的主题。谢谢你的好问题!

我一直在想:堆栈的目的是什么?

我假设您指的是 MSIL 语言的评估堆栈,而不是运行时的实际每线程堆栈

为什么会从内存转移到堆栈或“加载”?另一方面,为什么会有从堆栈到内存或“存储”的转移?为什么不把它们都放在内存中呢?

MSIL 是一种“虚拟机”语言。像 C# 编译器这样的编译器会生成 CIL,然后在运行时,另一个称为 JIT(实时)编译器的编译器将 IL 转换为可以执行的实际机器代码。

因此,首先让我们回答“为什么有MSIL”这个问题?为什么不让 C# 编译器写出机器代码呢?

因为这样做更便宜。假设我们没有那样做;假设每种语言都必须有自己的机器码生成器。您有 20 种不同的语言:C#、JScript .NET、Visual Basic、IronPythonF#...假设你有十个不同的处理器。你需要编写多少个代码生成器?20 x 10 = 200 个代码生成器。这是一项艰巨的工作。现在假设您要添加一个新处理器。你必须为它编写代码生成器 20 次,每种语言一次。

此外,这是一项艰巨而危险的工作。为您不熟悉的芯片编写高效的代码生成器是一项艰巨的工作!编译器设计人员是语言语义分析方面的专家,而不是新芯片组的有效寄存器分配方面的专家。

现在假设我们以 CIL 方式进行操作。您必须编写多少个 CIL 生成器?每种语言一个。您必须编写多少个 JIT 编译器?每个处理器一个。总计:20 + 10 = 30 个代码生成器。此外,语言到 CIL 生成器很容易编写,因为 CIL 是一种简单的语言,而 CIL 到机器代码生成器也很容易编写,因为 CIL 是一种简单的语言。我们摆脱了 C# 和 VB 等的所有复杂性,并将所有内容“简化”为一种易于编写抖动的简单语言。

拥有中间语言可以大大降低生产新语言编译器的成本。它还大大降低了支持新芯片的成本。你想支持一个新芯片,你找到一些芯片上的专家,让他们写一个CIL抖动,你就完成了;然后,您可以在芯片上支持所有这些语言。

好的,我们已经确定了为什么我们有 MSIL;因为拥有中间语言可以降低成本。那么,为什么语言是“堆栈机器”呢?

因为堆栈机器在概念上对于语言编译器编写者来说非常简单。堆栈是一种简单易懂的计算描述机制。从概念上讲,堆栈机器对于JIT编译器编写者来说也非常容易处理。使用堆栈是一种简化的抽象,因此,它再次降低了我们的成本

你问“为什么要有堆栈?为什么不直接在内存中做所有事情呢?好吧,让我们考虑一下。假设您要为以下项生成 CIL 代码:

int x = A() + B() + C() + 10;

假设我们有一个约定,即“add”、“call”、“store”等总是将它们的参数从堆栈中取出,并将它们的结果(如果有的话)放在堆栈上。为了生成此 C# 的 CIL 代码,我们只需这样说:

load the address of x // The stack now contains address of x
call A()              // The stack contains address of x and result of A()
call B()              // Address of x, result of A(), result of B()
add                   // Address of x, result of A() + B()
call C()              // Address of x, result of A() + B(), result of C()
add                   // Address of x, result of A() + B() + C()
load 10               // Address of x, result of A() + B() + C(), 10
add                   // Address of x, result of A() + B() + C() + 10
store in address      // The result is now stored in x, and the stack is empty.

现在假设我们在没有堆栈的情况下做到了。我们将按照您的方式进行操作,其中每个操作码都采用其操作数的地址以及存储结果的地址

Allocate temporary store T1 for result of A()
Call A() with the address of T1
Allocate temporary store T2 for result of B()
Call B() with the address of T2
Allocate temporary store T3 for the result of the first addition
Add contents of T1 to T2, then store the result into the address of T3
Allocate temporary store T4 for the result of C()
Call C() with the address of T4
Allocate temporary store T5 for result of the second addition
...

你看这是怎么回事吗?我们的代码变得越来越庞大,因为我们必须显式分配所有临时存储,而这些存储通常按照惯例只会放在堆栈上。更糟糕的是,我们的操作码本身都变得越来越大,因为它们现在都必须将要写入结果的地址以及每个操作数的地址作为参数。一个“add”指令知道它将从堆栈中删除两个东西并放置一个东西,可以是一个字节。一个需要两个操作数地址和一个结果地址的添加指令将是巨大的。

我们使用基于堆栈的操作码,因为堆栈解决了常见问题。即:我想分配一些临时存储空间,尽快使用它,然后在完成后迅速将其删除。通过假设我们有一个堆栈可供使用,我们可以使操作码非常小,代码非常简洁。

更新:一些其他想法

顺便说一句,通过(1)指定虚拟机,(2)编写面向VM语言的编译器,以及(3)在各种硬件上编写VM的实现来大幅降低成本的想法根本不是一个新想法。它不是起源于 MSIL、LLVM、Java 字节码或任何其他现代基础设施。据我所知,这种策略的最早实现是 1966 年的 pcode 机器

我个人第一次听说这个概念是在我了解到 Infocom 实现者如何设法让 Zork 在这么多不同的机器上运行得如此出色时。他们指定了一个名为 Z-machine 的虚拟机,然后为他们想要运行游戏的所有硬件制作了 Z-machine 模拟器。这还有一个额外的巨大好处,即他们可以在原始的 8 位系统上实现虚拟内存管理;游戏可能比内存大,因为他们可以在需要时从磁盘中分页代码,并在需要加载新代码时丢弃它。

评论

63赞 Jan Carlo Viray 10/24/2011
哇。这正是我想要的。获得答案的最好方法是从主要开发人员本人那里获得答案。感谢您抽出时间接受采访,我相信这将帮助所有想知道编译器和 MSIL 复杂性的人。谢谢埃里克。
18赞 jprete 10/25/2011
这是一个很好的答案。提醒我为什么即使我是 Java 人,我也会阅读您的博客。;-)
34赞 Eric Lippert 10/25/2011
@JanCarloViray:不客气!我注意到我是首席开发人员,而不是首席开发人员。这个团队里有几个人拥有这个职位,而我甚至不是他们中最资深的。
17赞 Alan 10/25/2011
@Eric:如果/当你不再热爱编码时,你应该考虑去教程序员。除了乐趣之外,您还可以在没有业务压力的情况下大赚一笔。令人敬畏的天赋是你在那个领域得到的(以及美妙的耐心,我可能会补充)。我是作为前大学讲师这么说的。
19赞 Binary Worrier 10/26/2011
大约有 4 段我对自己说“这听起来像埃里克”,到 5 或 6 岁时,我已经毕业了“是的,绝对是埃里克”:)另一个真正史诗般的全面答案。
8赞 skyman 10/26/2011 #5

在堆栈问题中添加更多内容。堆栈概念源自 CPU 设计,其中算术逻辑单元 (ALU) 中的机器代码在位于堆栈上的操作数上运行。例如,乘法运算可能会从堆栈中获取两个排名靠前的操作数,将它们乘以并将结果放回堆栈中。机器语言通常有两个基本函数,用于在堆栈中添加和删除操作数;推和弹出。在许多CPU的DSP(数字信号处理器)和机器控制器(例如控制洗衣机的控制器)中,堆栈位于芯片本身上。这样可以更快地访问 ALU,并将所需的功能整合到单个芯片中。

4赞 Basile Starynkevitch 10/29/2011 #6

通过使用延续传递编码样式,可以使系统在没有堆栈的情况下工作。然后,调用帧成为在垃圾回收堆中分配的延续(垃圾回收器需要一些堆栈)。

参见 Andrew Appel 的旧著作: 使用延续垃圾回收进行编译可以比堆栈分配更快

(由于缓存问题,他今天可能有点不对劲)

0赞 MicroservicesOnDDD 6/6/2020 #7

我寻找“中断”,但没有人将其视为优势。对于中断微控制器或其他处理器的每个设备,通常会将寄存器推送到堆栈上,调用中断服务例程,完成后,将寄存器从堆栈中弹出,并放回原处。然后指令指针被恢复,正常活动从中断的地方继续,几乎就像中断从未发生过一样。有了堆栈,你实际上可以让几个设备(理论上)相互中断,而且这一切都可以正常工作——因为堆栈。

还有一系列基于堆栈的语言,称为串联语言。它们都是(我相信)函数式语言,因为堆栈是传入的隐式参数,而且更改后的堆栈是每个函数的隐式返回。ForthFactor(非常出色)都是示例,还有其他示例。Factor 的使用类似于 Lua,用于编写游戏脚本,由目前在 Apple 工作的天才 Slava Pestov 编写。他在 youtube 上的 Google TechTalk 我已经看了几次。他谈到了 Boa 构造函数,但我不确定他的意思;

我真的认为当前的一些VM,比如JVM,Microsoft的CIL,甚至是我看到的为Lua编写的VM,应该用一些基于堆栈的语言编写,以使它们可移植到更多的平台。我认为这些连接语言在某种程度上缺少了它们作为 VM 创建工具包和可移植性平台的使命。甚至还有pForth,一个用ANSI C编写的“可移植”Forth,可用于更通用的可移植性。有人尝试使用 Emscripten 或 WebAssembly 编译它吗?

对于基于堆栈的语言,有一种称为零点的代码样式,因为您可以只列出要调用的函数,而无需(有时)传递任何参数。如果这些函数完美地结合在一起,那么你将只剩下所有零点函数的列表,这将是你的应用程序(理论上)。如果你深入研究 Forth 或 Factor,你就会明白我在说什么。

在Easy Forth,一个用JavaScript编写的不错的在线教程中,这里有一个小示例(请注意“sq sq sq sq”作为零点调用风格的示例):

: sq dup * ;  ok
2 sq . 4  ok
: ^4 sq sq ;  ok
2 ^4 . 16  ok
: ^8 sq sq sq sq ;  ok
2 ^8 . 65536  ok

此外,如果你看一下 Easy Forth 网页源代码,你会在底部看到它非常模块化,用大约 8 个 JavaScript 文件编写。

我花了很多钱买了几乎所有我能拿到的福斯书,试图吸收福斯,但现在我才刚刚开始更好地理解它。我想给后来的人提个醒,如果你真的想得到它(我发现这一点为时已晚),请获取关于 FigForth 的书并实现它。商业 Forths 太复杂了,而 Forth 最伟大的事情是可以理解整个系统,从上到下。不知何故,Forth 在新处理器上实现了整个开发环境,尽管 C 似乎已经满足了对这种需求的所有需求,但从头开始编写 Forth 仍然是一种有用的仪式。所以,如果你选择这样做,试试 FigForth 一书——它是在各种处理器上同时实现的几个 Forth。一种福斯的罗塞塔石碑。

为什么我们需要一个堆栈——效率、优化、零点、中断时保存寄存器,对于递归算法来说,它是“正确的形状”。