What is the purpose of a stack? Why do we need it?
因此,我现在正在学习MSIL,以了解如何调试我的C.NET应用程序。
我一直在想:这堆东西的目的是什么?
只是把我的问题放在上下文中:
为什么要从内存转移到堆栈或"加载"?另一方面,为什么会有从堆栈到内存或"存储"的传输?为什么不把它们都放在记忆里呢?
- 是因为它更快吗?
- 是因为它是基于RAM的吗?
- 为了效率?
我正试图抓住这一点来帮助我更深入地理解CIL代码。
更新:我非常喜欢这个问题,我在2011年11月18日把它作为我博客的主题。谢谢你的提问!好的。
I've always wondered: what is the purpose of the stack?
Ok.
我假设您是指MSIL语言的评估堆栈,而不是运行时每个线程的实际堆栈。好的。
Why is there a transfer from memory to stack or"loading?" On the other hand, why is there a transfer from stack to memory or"storing"? Why not just have them all placed in the memory?
Ok.
MSIL是一种"虚拟机"语言。像C编译器这样的编译器生成CIL,然后在运行时另一个名为JIT(及时)编译器的编译器将IL转换为可以执行的实际机器代码。好的。
首先,让我们回答一个问题:"为什么要有MSIL?"为什么不让C编译器写出机器代码呢?好的。
因为这样做比较便宜。假设我们没有这样做;假设每种语言都必须有自己的机器代码生成器。您有20种不同的语言:C、jscript.net、Visual Basic、Ironpython、F……假设你有十个不同的处理器。你需要写多少代码生成器?20 x 10=200代码生成器。这是很多工作。现在假设您想要添加一个新的处理器。您必须为它编写20次代码生成器,每种语言一次。好的。
此外,这是一项困难和危险的工作。为不是专家的芯片编写高效的代码生成器是一项艰巨的工作!编译器设计者是语言语义分析方面的专家,而不是新芯片集的有效寄存器分配方面的专家。好的。
现在假设我们用CIL的方式。你要写多少CIL生成器?每种语言一个。您需要编写多少个JIT编译器?每个处理器一个。总计:20+10=30个代码生成器。此外,CIL生成器的语言很容易编写,因为CIL是一种简单的语言,而CIL到机器代码生成器也很容易编写,因为CIL是一种简单的语言。我们摆脱了所有复杂的C和VB,以及"不"和"低"的东西,一切都变成了一种简单的语言,很容易为其编写抖动。好的。
拥有一种中间语言大大降低了生成新语言编译器的成本。它还大大降低了支持新芯片的成本。如果你想支持一个新的芯片,你可以在该芯片上找到一些专家,让他们写一个CIL抖动,你就完成了;然后你就可以支持芯片上的所有语言。好的。
好吧,我们已经确定了为什么要使用MSIL;因为使用中间语言可以降低成本。为什么这种语言是"堆栈机器"?好的。
因为堆栈机器在概念上对于语言编译器编写者来说非常简单。堆栈是一种简单易懂的描述计算的机制。对于JIT编译器编写者来说,堆栈机器在概念上也非常容易处理。使用堆栈是一种简化的抽象,因此它再次降低了我们的成本。好的。
你问"为什么有一堆?"为什么不直接从记忆中做每件事呢?好吧,让我们考虑一下。假设要为以下项生成CIL代码:好的。
1 | int x = A() + B() + C() + 10; |
假设我们有这样的约定:"添加"、"调用"、"存储"等等总是将它们的参数从堆栈中去掉,并将它们的结果(如果有)放到堆栈中。要为这个C生成CIL代码,我们只需要说:好的。
1 2 3 4 5 6 7 8 9 | 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. |
现在假设我们没有一叠纸就完成了。我们将按您的方式进行,其中每个操作码获取其操作数的地址和存储其结果的地址:好的。
1 2 3 4 5 6 7 8 9 10 | 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)编写面向虚拟机语言的编译器,以及(3)在各种硬件上编写虚拟机的实现来大幅降低成本的想法根本不是一个新想法。它不是源于MSIL、LLVM、Java字节码或任何其他现代基础设施。我所知道的这个策略最早的实现是1966年的pcode机器。好的。
我个人第一次听说这个概念是在我了解到Infocom实现者如何让Zork在这么多不同的机器上运行得如此好的时候。他们指定了一个名为z-machine的虚拟机,然后为他们想要运行游戏的所有硬件制作了z-machine模拟器。这有一个额外的巨大好处,那就是他们可以在原始的8位系统上实现虚拟内存管理;一个游戏可能比适合内存的要大,因为他们可以在需要的时候从磁盘上调出代码,在需要加载新代码的时候丢弃它。好的。好啊。
请记住,当您谈论MSIL时,您谈论的是虚拟机的指令。.NET中使用的虚拟机是基于堆栈的虚拟机。与基于寄存器的虚拟机不同,Android操作系统中使用的Dalvik虚拟机就是一个例子。
虚拟机中的堆栈是虚拟的,由解释器或实时编译器将虚拟机指令转换为在处理器上运行的实际代码。在.NET的情况下,几乎总是一个抖动,MSIL指令集被设计为从GET GO开始抖动。与Java字节码相反,它对特定数据类型的操作有不同的说明。这使得它被优化以便于解释。然而,实际上存在一个MSIL解释器,它在.NET微框架中使用。它运行在资源非常有限的处理器上,负担不起存储机器代码所需的RAM。
实际的机器代码模型是混合的,既有堆栈又有寄存器。JIT代码优化器的一项重要工作是找到将保存在堆栈中的变量存储在寄存器中的方法,从而大大提高执行速度。Dalvik抖动有相反的问题。
机器堆栈是一个非常基本的存储设备,在处理器设计中已经存在很长时间了。它具有很好的引用位置,这是现代CPU上的一个非常重要的特性,它可以以比RAM更快的速度读取数据,并支持递归。语言设计很大程度上受堆栈的影响,堆栈在支持局部变量和方法体范围内是可见的。堆栈的一个重要问题是该站点的名称。
有一篇非常有趣/详细的维基百科文章,关于栈机指令集的优势。我需要完全引用它,所以简单地放一个链接更容易。我只引用子标题
- 非常紧凑的目标代码
- 简单编译器/简单解释程序
- 最小处理器状态
在叠加问题上再加一点。堆栈概念源自CPU设计,其中算术逻辑单元(ALU)中的机器代码对位于堆栈上的操作数进行操作。例如,乘法运算可以从堆栈中提取两个最上面的操作数,并将它们相乘,然后将结果放回堆栈中。机器语言通常有两个基本函数来添加和删除堆栈中的操作数:push和pop。在许多CPU的DSP(数字信号处理器)和机器控制器(如控制洗衣机)中,堆栈位于芯片本身。这样就可以更快地访问ALU,并将所需的功能整合到单个芯片中。
如果不遵循堆栈/堆的概念,而将数据加载到随机内存位置,或者从随机内存位置存储数据…它将是非常非结构化和非托管的。
这些概念用于以预先定义的结构存储数据,以提高性能、内存使用率…因此称为数据结构。
通过使用连续传递的编码方式,可以让系统在没有堆栈的情况下工作。然后,调用帧成为垃圾收集堆中分配的连续性(垃圾收集器需要一些堆栈)。
请参阅AndrewAppel的旧著作:使用延续和垃圾收集进行编译比堆栈分配更快。
(他今天可能有点错,因为缓存问题)