使用堆栈表示函数调用语义的替代方法有哪些?


19

我们都知道并喜欢函数调用通常是使用堆栈来实现的。有框架,返回地址,参数等等。

但是,堆栈是实现细节:调用约定可能会做不同的事情(例如,x86快速调用使用(某些)寄存器,MIPS和跟随器使用寄存器窗口,等等),优化甚至可以做其他事情(内联,帧指针省略,尾调用优化..)。

当然,许多机器(如JVM和CLR这样的VM,以及带有PUSH / POP等的x86等实际机器)上都存在方便的堆栈指令,可以很方便地将其用于函数调用,但是在某些情况下,这是可能的以不需要调用堆栈的方式进行编程(我在这里考虑的是Continuation Passing Style,或者消息传递系统中的Actor)

因此,我开始感到奇怪:是否可以在没有堆栈的情况下实现函数调用语义,或者更好地使用不同的数据结构(可能是队列,或者是关联映射?)
,当然,我知道堆栈非常有用。方便(它无处不在的一个原因),但是最近我碰到了一个使我感到奇怪的实现。

你们是否知道是否曾经用任何语言/机器/虚拟机完成过这项工作?如果有的话,有哪些显着的区别和缺点?

编辑:我的直觉是不同的子计算方法可以使用不同的数据结构。例如,lambda演算不是基于堆栈的(函数应用程序的想法通过简化来体现),但是我正在查看一种真实的语言/机器/示例。这就是为什么我要问...


Clean使用图和图重写机,而图改写机又使用三栈计算机实现,但内容与通常不同。

对于虚拟机,可以使用链表。列表的每个节点都是一个框架。由于虚拟机使用了硬件堆栈,因此可以在没有开销的情况下将帧存在于堆中realloc()
shawnhcorey

Answers:


19

根据语言,可能没有必要使用调用堆栈。调用堆栈仅在允许递归或相互递归的语言中是必需的。如果该语言不允许递归,则任何时候任何过程的一次调用都可能处于活动状态,并且可以静态分配该过程的局部变量。此类语言的确必须为上下文更改,中断处理做好准备,但这仍然不需要堆栈。

有关不需要调用堆栈的语言的示例,请参阅FORTRAN IV(及更早版本)和早期版本的COBOL。

请参阅Control Data 6600(及更早的Control Data机器),以获取不为调用堆栈提供直接硬件支持的高度成功的早期超级计算机的示例。有关非常成功的早期小型计算机(不支持调用堆栈)的示例,请参阅PDP-8。

据我所知,Burroughs B5000堆栈计算机是第一批具有硬件调用堆栈的计算机。B5000机器是从头开始设计的,可以运行ALGOL,这需要递归。他们还拥有最早的基于描述符的架构之一,这为能力架构奠定了基础。

据我所知,是PDP-6(后来发展为DEC-10)普及了呼叫堆栈硬件,当时麻省理工学院的黑客社区交付了其中的一个,并发现PUSHJ(推回地址和跳转)操作允许将十进制打印例程从50条指令减少到10条指令。

允许递归的语言中最基本的函数调用语义要求与堆栈很好匹配的功能。如果这就是您所需要的,那么基本堆栈就是一个很好的简单匹配。如果您还需要更多,那么您的数据结构就必须做更多的事情。

我遇到的更多需求的最佳示例是“继续”,即在中间暂停计算,将其保存为冻结状态气泡并在以后(可能多次)再次触发它的能力。在LISP的方言中,延续是流行的一种实现错误出口的方式。连续性要求能够对当前执行环境进行快照,并在以后进行再现,而为此,堆栈就显得有些不便。

Abelson&Sussman的“计算机程序的结构和解释”详细介绍了延续性。


2
这是一个伟大的历史见解,谢谢!当我问我的问题时,我确实想到了延续性,尤其是延续性通过风格(CPS)。在这种情况下,堆栈不仅不方便,而且可能不是必需的:您无需记住要返回的位置,只需提供一个位置即可继续执行。我想知道其他无堆栈方法是否通用,而您给出了一些我不知道的非常好的方法。
LorenzoDematté2013年

稍微相关:您正确指出“如果该语言不允许递归”。带有递归的语言呢,特别是那些非尾递归的函数呢?他们是否需要“根据设计”堆叠?
罗伦佐·德马特(LorenzoDematté),2013年

“调用堆栈仅在允许递归或相互递归的语言中是必需的”-否。如果一个函数可以从多个地方被调用(比如他们foobar可调用baz),则函数需要知道什么返回。如果嵌套此“谁将返回”信息,则最终将导致堆栈。不管它叫什么,或者它是否受CPU硬件的支持,还是您在软件中模拟的东西(甚至是它的静态分配条目的链表),都仍然是堆栈。
布伦丹2013年

@Brendan不一定(至少,这是我问题的全部目的)。“要返回哪里”或更好的“下一步要去”是否需要堆栈,即LIFO结构?可能是树,地图,队列或其他东西吗?
LorenzoDematté13年

例如,我的直觉是CPS只需要一棵树,但是我不确定,也不知道在哪里看。这就是为什么我问..
洛伦佐Dematté

6

不使用某种堆栈就无法实现函数调用语义。只能玩文字游戏(例如,使用其他名称,例如“ FILO返回缓冲区”)。

可以使用一些没有实现函数调用语义的东西(例如,连续传递样式,参与者),然后在其之上构建函数调用语义。但这意味着添加某种数据结构以跟踪函数返回时将控制权传递到何处,并且该数据结构将是一种堆栈类型(或名称/描述不同的堆栈)。

假设您有许多可以互相调用的函数。在运行时,每个函数都必须知道该函数退出时要返回的位置。如果有first电话,second您将:

second returns to somewhere in first

然后,如果有second电话,third您将:

third returns to somewhere in second
second returns to somewhere in first

然后,如果有third电话,fourth您将:

fourth returns to somewhere in third
third returns to somewhere in second
second returns to somewhere in first

调用每个函数时,必须将更多的“返回位置”信息存储在某个位置。

如果函数返回,那么将使用并且不再需要其“返回位置”信息。例如,如果fourth返回到某处,third则“返回到何处”信息的数量将变为:

third returns to somewhere in second
second returns to somewhere in first

基本上; “函数调用语义”意味着:

  • 您必须具有“返回地点”信息
  • 信息量随着函数的调用而增加,并在函数返回时减少
  • 存储的第一条“返回位置”信息将是丢弃的最后一条“返回位置”信息

这描述了FILO / LIFO缓冲区或堆栈。

如果尝试使用一种树,则树中的每个节点将永远不会有多个子节点。注:如果一个函数调用2层或更多的功能与多个孩子的节点只能发生在同一时间,这需要某种并发性(如螺纹,叉()等),它不会是“函数调用的语义”。如果树中的每个节点永远都不会超过一个子节点;那么该“树”将仅用作FILO / LIFO缓冲区或堆栈;并且由于它仅用作FILO / LIFO缓冲区或堆栈,因此可以断言“树”是堆栈(唯一的区别是文字游戏和/或实现细节)。

这同样适用于可以想象用来实现“函数调用语义”的任何其他数据结构-它将用作堆栈(唯一的区别是文字游戏和/或实现细节);除非它破坏了“函数调用语义”。注意:如果可以的话,我将提供其他数据结构的示例,但我认为没有任何其他合理的结构。

当然,如何实现堆栈是实现细节。它可能是一个内存区域(您在其中跟踪“当前堆栈顶部”),它可能是某种类型的链接列表(在此您跟踪“列表中的当前条目”),或者可以在某些区域中实现另一种方式。硬件是否具有内置支持也无关紧要。

注意:在任何时候,如果只有一个过程的一次调用处于活动状态;那么您可以为“返回地点”信息静态分配空间。这仍然是一个堆栈(例如,以FILO / LIFO方式使用的静态分配条目的链表)。

还要注意,有些事情不遵循“函数调用语义”。这些内容包括“可能非常不同的语义”(例如,连续传递,参与者模型);并且还包括对“函数调用语义”的通用扩展,例如并发(线程,纤维等),setjmp/ longjmp,异常处理等。


根据定义,堆栈是LIFO集合:后进先出。队列是FIFO集合。
John R. Strohm 2013年

那么堆栈是唯一可接受的数据结构吗?如果是这样,为什么?
LorenzoDematté13年

@ JohnR.Strohm:固定:-)
Brendan

1
对于没有递归的语言(直接或多语言),可以为每个方法静态分配一个变量,该变量将标识上次调用该方法的位置。如果链接器知道这些事情,它可以用一种方式分配这样的变量,这种方式不会比如果实际采用每个静态可能的执行路径时堆栈所做的事情更糟。
超级猫

4

玩具级联语言XY使用调用队列和数据堆栈来执行。

每个计算步骤都只涉及确定要执行的下一个单词,对于内置函数,将其内部函数作为参数提供给数据堆栈和调用队列,或者使用userdef将其组成的单词推入队列的最前面。

因此,如果我们有一个将顶部元素加倍的功能:

; double dup + ;
// defines 'double' to be composed of 'dup' followed by '+'
// dup duplicates the top element of the data stack
// + pops the top two elements and push their sum

然后是组合函数,+dup具有以下堆栈/队列类型签名:

// X is arbitraty stack, Y is arbitrary queue, ^ is concatenation
+      [X^a^b Y] -> [X^(a + b) Y]
dup    [X^a Y] -> [X^a^a Y]

矛盾的是,double看起来像这样:

double [X Y] -> [X dup^+^Y]

因此从某种意义上讲,XY是无堆栈的。


哇谢谢!我将研究...不确定它是否真的适用于函数调用,但还是值得一看
LorenzoDematté2013年

1
@ Karl Damgaard Asmussen“将构成单词的单词推到队列的最前面”“推前面”这不是一个堆栈吗?

@ guesttttttt222222222并非如此。调用堆栈存储返回指针,并在函数返回时弹出调用堆栈。执行队列仅存储指向函数的指针,并且在执行下一个函数时,它会扩展到其定义并推送到队列的前面。在XY中,执行队列实际上是双端队列,因为有些操作也在执行队列的后面进行。
Karl Damgaard Asmussen,2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.