因此,我现在正在学习MSIL,以学习调试C#.NET应用程序。
我一直想知道:堆栈的目的是什么?
只是为了回答我的问题:
为什么要从内存转移到堆栈或“加载”?另一方面,为什么要从堆栈转移到内存或“存储”呢?
为什么不将它们全部放置在内存中?
- 是因为速度更快吗?
- 是因为它基于RAM吗?
- 为了效率?
我试图抓住这一点,以帮助我更深入地了解CIL代码。
因此,我现在正在学习MSIL,以学习调试C#.NET应用程序。
我一直想知道:堆栈的目的是什么?
只是为了回答我的问题:
为什么要从内存转移到堆栈或“加载”?另一方面,为什么要从堆栈转移到内存或“存储”呢?
为什么不将它们全部放置在内存中?
我试图抓住这一点,以帮助我更深入地了解CIL代码。
Answers:
更新:我非常喜欢这个问题,因此在2011年11月18日将其作为博客主题。感谢您提出的好问题!
我一直想知道:堆栈的目的是什么?
我假设您的意思是MSIL语言的评估堆栈,而不是运行时的实际每线程堆栈。
为什么要从内存转移到堆栈或“加载”?另一方面,为什么要从堆栈转移到内存或“存储”呢?为什么不将它们全部放置在内存中?
MSIL是一种“虚拟机”语言。像C#编译器这样的编译器会生成CIL,然后在运行时另一个称为JIT(即时)的编译器会将IL转换为可以执行的实际机器代码。
因此,首先让我们回答“为什么要拥有MSIL”这个问题。为什么不让C#编译器写出机器代码?
因为这样做比较便宜。假设我们没有那样做;假设每种语言都必须具有自己的机器代码生成器。您有二十种不同的语言: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代码:
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位系统上实现虚拟内存管理。一个游戏可能比适合内存的游戏大,因为他们可以在需要时从磁盘分页代码,而在需要加载新代码时将其丢弃。
请记住,当您谈论MSIL时,您是在谈论虚拟机的指令。.NET中使用的VM是基于堆栈的虚拟机。与基于寄存器的VM相对,Android操作系统中使用的Dalvik VM是一个例子。
VM中的堆栈是虚拟的,这取决于解释器或即时编译器将VM指令转换为在处理器上运行的实际代码。在.NET的情况下,这几乎总是抖动,MSIL指令集被设计为一开始就被抖动。例如,与Java字节码相反,它具有针对特定数据类型进行操作的不同指令。这使得它经过优化可以解释。尽管实际上存在一个MSIL解释器,但它已在.NET Micro Framework中使用。它在资源非常有限的处理器上运行,无法承受存储机器代码所需的RAM。
实际的机器代码模型是混合的,具有堆栈和寄存器。JIT代码优化器的一项重要工作是想出一种方法来将保留在堆栈中的变量存储在寄存器中,从而大大提高了执行速度。Dalvik抖动有相反的问题。
否则,机器堆栈是非常基本的存储设备,已经在处理器设计中使用了很长时间。它具有很好的参考位置,这是现代CPU上的一个非常重要的功能,它可以以比RAM提供数据更快的速度读取数据并支持递归。语言设计在很大程度上受堆栈的影响,堆栈对于支持局部变量和范围限于方法主体可见。堆栈的一个重要问题是该站点的名称。
对此,有一篇非常有趣/详细的Wikipedia文章:堆栈机指令集的优点。我需要全部引用它,因此简单地放置一个链接会更容易。我只引用字幕
我在寻找“中断”,没有人将其作为优势。对于每个中断微控制器或其他处理器的设备,通常会有一些寄存器被压入堆栈,调用中断服务程序,完成后,将这些寄存器从堆栈中弹出,并放回它们的位置。是。然后恢复指令指针,正常活动从中断处恢复,几乎就像中断从未发生过一样。使用堆栈,实际上(理论上)您可以让多个设备互相中断,并且由于堆栈的缘故,它们都可以正常工作。
还有一系列基于堆栈的语言,称为连接语言。它们都是(我相信)功能语言,因为堆栈是传入的隐式参数,而且更改后的堆栈是每个函数的隐式返回。两个第四和因子(其是优异的)是一个例子,与他人一起。Factor已与Lua相似地用于脚本游戏,并且由目前在Apple工作的天才Slava Pestov编写。我看过他在youtube上的Google TechTalk。他谈论Boa构造函数,但我不确定他是什么意思;-)。
我真的认为,当前的某些VM,例如JVM,Microsoft的CIL,甚至我所见的VM都是为Lua编写的,都应该使用某些基于堆栈的语言编写,以使其可移植到更多平台上。我认为这些连接语言在某种程度上缺少了作为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文件编写。
我几乎花了很多钱在每本Forth书上,我可以尝试吸收它,但是现在我才开始更好地理解它。我想跟进来的人,如果您真的想要得到它(我发现这太晚了),请下载关于FigForth的书并实现它。商业化的Forth太复杂了,而Forth的最大优点是可以从上到下理解整个系统。尽管需要,Forth可以以某种方式在新处理器上实现整个开发环境。因为C似乎在所有方面都已通过,所以从头开始编写Forth仍然是一种通过仪式。因此,如果您选择执行此操作,请尝试一下FigForth书-这是在各种处理器上同时实现的几个Forth。一种罗塞塔石碑。
我们为什么需要堆栈-效率,优化,零点,在中断时保存寄存器,对于递归算法来说,它是“正确的形状”。