堆栈的目的是什么? 我们为什么需要它?
所以我现在正在学习MSIL来学习调试我的C#.NET应用程序。
我一直在想: 堆栈的目的是什么?
只要把我的问题放在上下文中:
为什么有从内存转移到堆栈或“加载”? 另一方面,为什么有从堆栈转移到内存或“存储”? 为什么不把它们都放在内存中?
我试图理解这一点,以帮助我更深入地理解CIL代码。
更新:我非常喜欢这个问题,我于2011年11月18日将它作为我博客的主题。谢谢你的好问题!
我一直在想:堆栈的目的是什么?
我假设你是指MSIL语言的评估堆栈,而不是运行时的实际每线程堆栈。
为什么有从内存转移到堆栈或“加载”? 另一方面,为什么有从堆栈转移到内存或“存储”? 为什么不把它们都放在内存中?
MSIL是一种“虚拟机”语言。 编译器如C#编译器生成CIL,然后在运行时,另一个称为JIT(Just In Time)编译器的编译器将IL转换为可以执行的实际机器代码。
所以首先让我们回答一个问题:“为什么要有MSIL?” 为什么不让C#编译器写出机器代码?
因为这样做更便宜。 假设我们没有那样做, 假设每种语言都必须有自己的机器代码生成器。 你有二十种不同的语言:C#,JScript .NET,Visual Basic,IronPython,F#......假设你有十个不同的处理器。 你需要编写多少个代码生成器? 20 x 10 = 200个代码生成器。 这是很多工作。 现在假设你想添加一个新的处理器。 你必须为它编写二十次代码生成器,每种语言一个。
此外,这是艰难而危险的工作。 为您不擅长的芯片编写高效的代码生成器是一项艰巨的任务! 编译器设计人员是他们语言语义分析的专家,而不是新型芯片组的有效寄存器分配。
现在假设我们采用CIL方式。 你需要写多少个CIL发生器? 每种语言一个。 您需要编写多少个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”指令可以是单个字节。 带有两个操作数地址和一个结果地址的add指令将会非常庞大。
我们使用基于堆栈的操作码,因为堆栈可以解决常见问题。 即: 我想分配一些临时存储空间,尽快使用它,然后在完成时快速清除它 。 通过假设我们有一个栈,我们可以使操作码非常小,代码非常简洁。
更新:一些额外的想法
顺便说一下,这种通过(1)指定虚拟机,(2)编写以VM语言为目标的编译器,以及(3)在各种硬件上编写VM的实现方式,大幅降低成本的想法根本不是一个新想法。 它不是源于MSIL,LLVM,Java字节码或任何其他现代基础设施。 我知道这个策略的最早实施是1966年的pcode机器。
我个人第一次听说这个概念是当我了解到Infocom的实现者如何设法使Zork在如此多的不同机器上运行得如此之好。 他们指定了一台名为Z-machine的虚拟机,然后为所有想要运行游戏的硬件制作Z-machine仿真器。 这增加了巨大的好处,可以在原始的8位系统上实现虚拟内存管理; 一个游戏可能比适合内存的游戏更大,因为当他们需要时可以从磁盘中将代码分页,并在需要加载新代码时将其丢弃。
请记住,当你在谈论MSIL时,你正在讨论关于虚拟机的说明。 .NET中使用的虚拟机是基于堆栈的虚拟机。 与基于注册的VM相反,Android操作系统中使用的Dalvik VM就是一个例子。
虚拟机中的堆栈是虚拟的,由解释器或即时编译器来将VM指令翻译成运行在处理器上的实际代码。 在.NET的情况下,几乎总是一个抖动,MSIL指令集被设计为随时可用。 例如,与Java字节码相反,它对特定数据类型的操作有不同的指示。 这使得它被优化来解释。 尽管MSIL解释器实际上存在,但它在.NET Micro Framework中使用。 它运行在资源非常有限的处理器上,无法承担存储机器代码所需的RAM。
实际的机器码模型是混合的,同时具有堆栈和寄存器。 JIT代码优化器的一个重要工作就是想办法将保存在堆栈中的变量存储在寄存器中,从而大大提高执行速度。 达尔维克抖动有相反的问题。
机器堆栈是非常基本的存储设备,已经在处理器设计中使用了很长时间。 它具有非常好的参考地位,这是现代CPU的一个非常重要的特性,它比RAM更快速地扫描数据,并支持递归。 语言设计很大程度上受到一个堆栈的影响,可见支持局部变量和范围仅限于方法体。 该堆栈的一个重大问题是该站点命名的问题。
有一个非常有趣/详细的维基百科文章,关于这个,堆栈机器指令集的优点。 我需要完全引用它,所以简单地放一个链接就容易了。 我会简单地引用子标题