堆栈的目的是什么? 我们为什么需要它?
所以我现在正在学习MSIL来学习debugging我的C#.NET应用程序。
我一直在想: 堆栈的目的是什么?
只是把我的问题的背景:
为什么有从内存转移到堆栈或“加载”? 另一方面,为什么有从堆栈转移到内存或“存储”? 为什么不把它们都放在内存中?
- 是因为它快吗?
- 是因为它是基于RAM的吗?
- 为了效率?
我正在努力把握这一点,以帮助我更深入地理解CIL代码。
更新:我非常喜欢这个问题,于2011年11月18日成为我博客的主题 。 感谢您的好问题!
我一直在想:堆栈的目的是什么?
我假设你是指MSIL语言的评估堆栈 ,而不是运行时实际的每线程堆栈。
为什么有从内存转移到堆栈或“加载”? 另一方面,为什么有从堆栈转移到内存或“存储”? 为什么不把它们都放在内存中?
MSIL是一种“虚拟机”语言。 编译器如C#编译器生成CIL ,然后在运行时,另一个名为JIT(Just In Time)编译器的编译器将IL转换为可以执行的实际机器代码。
所以首先让我们回答“为什么要有MSIL? 为什么不只是让C#编译器写出机器码呢?
因为这样做比较便宜 。 假设我们没有这样做, 假设每种语言都必须有自己的机器码发生器。 你有二十种不同的语言:C#, JScript .NET ,Visual Basic, IronPython , F# …假设你有十个不同的处理器。 你需要编写多less个代码生成器? 20 x 10 = 200个代码生成器。 这是很多工作。 现在假设你想添加一个新的处理器。 你必须为它编写二十次代码生成器,每种语言一个。
此外,这是艰难而危险的工作。 为你不是专家的芯片写高效的代码生成器是一项艰巨的工作! 编译器devise者是他们语言语义分析的专家,而不是新的芯片组的有效寄存器分配。
现在假设我们采用CIL方式。 你需要写多less个CIL发生器? 每种语言一个。 你需要编写多less个JIT编译器? 每个处理器一个。 总计:20 + 10 = 30个代码生成器。 而且,由于CIL是一种简单的语言,CIL到机器代码生成器也易于编写,因为CIL是一种简单的语言,所以语言到CIL生成器很容易编写。 我们摆脱了所有错综复杂的C#和VB以及什么都不是,把所有东西都“简单化”为一个简单的语言,这个语言很容易写出抖动。
使用中间语言会大大降低生成新语言编译器的成本。 这也大大降低了支持新芯片的成本。 你想支持一个新的芯片,你会发现芯片上的一些专家,让他们写一个CIL抖动,你就完成了; 然后你在芯片上支持所有这些语言。
好的,所以我们已经确定了为什么我们有MSIL; 因为使用中间语言会降低成本。 为什么这个语言是一个“堆栈机器”?
因为堆栈机在语言编译器编写者处理概念上非常简单。 栈是描述计算的简单易懂的机制。 对于JIT编译器编写者来说,堆栈机器在概念上也很容易处理。 使用堆栈是一个简化的抽象,因此它又降低了我们的成本 。
你问“为什么有一堆?” 为什么不直接把所有的东西都直接拿出来呢? 那么,我们来考虑一下。 假设您想为以下内容生成CIL代码:
int x = A() + B() + C() + 10;
假设我们有“添加”,“调用”,“存储”等约定总是把他们的论点从堆栈中拿出来,并把它们的结果(如果有的话)放在堆栈上。 为了生成这个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位系统上实现虚拟内存pipe理 。 一个游戏可能会比内存大,因为他们可以在需要的时候从磁盘中将代码分页,并在需要加载新代码时将其丢弃。
请记住,当你在谈论MSIL的时候,你正在讨论关于虚拟机的说明。 .NET中使用的虚拟机是基于堆栈的虚拟机。 与基于注册的虚拟机相反,Android操作系统中使用的Dalvik VM就是一个例子。
VM中的堆栈是虚拟的,由解释器或即时编译器来将VM指令翻译成在处理器上运行的实际代码。 在.NET的情况下,几乎总是一个抖动,MSIL指令集被devise为从头开始。 例如,与Java字节码相反,它对特定数据types的操作有不同的指示。 这使得它被优化来解释。 实际上存在一个MSIL解释器,它在.NET Micro Framework中使用。 在资源非常有限的处理器上运行,无法承担存储机器代码所需的RAM。
实际的机器代码模型是混合的,既有堆栈又有寄存器。 JIT代码优化器的一个重要工作就是想办法将保存在堆栈中的variables存储在寄存器中,从而大大提高执行速度。 Dalvik抖动有相反的问题。
机器堆栈是非常基本的存储设备,已经在处理器devise中使用了很长时间。 它具有非常好的参考位置,在现代CPU上非常重要的一个特性,它比RAM能够提供更快的数据速度,并支持recursion。 语言devise很大程度上受到一个堆栈的影响,可见的是支持局部variables和范围仅限于方法体。 堆栈的一个重大问题是这个网站的名字。
有一个非常有趣的/详细的维基百科文章,关于这个, 堆栈机器指令集的优点 。 我需要完全引用它,所以简单地放一个链接就容易了。 我只是简单地引用子标题
- 非常紧凑的目标代码
- 简单的编译器/简单的解释器
- 最小的处理器状态
多添加一些堆栈问题。 堆栈概念源于CPUdevise,其中算术逻辑单元(ALU)中的机器代码对位于堆栈上的操作数进行操作。 例如,一个乘法操作可能会从堆栈中取出两个最高操作数,将它们复制并将结果放回堆栈。 机器语言通常有两个基本的function来添加和删除堆栈中的操作数; PUSH和POP。 在许多CPU的DSP(数字信号处理器)和机器控制器(如控制洗衣机)中,堆栈位于芯片本身。 这样可以更快地访问ALU,并将所需的function整合到单个芯片中。
如果没有遵循堆栈/堆的概念,数据被加载到随机存储器位置,或者数据是从随机存储器位置存储的,那么它将是非结构化和非托pipe的。
这些概念用于将数据存储在预定义的结构中,以提高性能,内存使用率…因此称为数据结构。
通过使用继续传递编码的风格 ,可以使系统工作在没有堆栈的情况下。 然后调用帧成为垃圾收集堆中分配的继续(垃圾收集器需要一些堆栈)。
请参阅Andrew Appel的旧着作: 使用Continuations和Garbage Collection 编译 可能比Stack Allocation更快
(由于caching问题,他今天可能会有点错误)