无堆语言如何工作?

时间:2009-06-19 03:22:14

标签: stack language-design stackless

我听说过无堆语言。但是,我不知道如何实现这样的语言。有人可以解释一下吗?

8 个答案:

答案 0 :(得分:85)

我们拥有的现代操作系统(Windows,Linux)以我称之为“大堆栈模型”的方式运行。有时,这种模式是错误的,并激发了对“无堆叠”语言的需求。

“大堆栈模型”假设编译的程序将为连续的内存区域中的函数调用分配“堆栈帧”,使用机器指令非常快速地调整包含堆栈指针(和可选的堆栈帧指针)的寄存器。这导致快速的函数调用/返回,代价是具有堆栈的大的连续区域。因为在这些现代操作系统下运行的所有程序中99.99%适用于大堆栈模型,编译器,加载器甚至操作系统“都知道”这个堆栈区域。

所有此类应用程序的一个常见问题是,“我的堆栈应该有多大?”。由于内存很便宜,所以大多数情况下会发生大量的数据块(MS默认为1Mb),而典型的应用程序调用结构永远不会接近使用它。但是如果一个应用程序确实全部使用它,它就会因非法内存引用而死(“我很抱歉Dave,我不能这样做”),因为它已经到了堆栈的末尾。

大多数所谓的“无堆叠”语言并非真正无堆叠。它们只是不使用这些系统提供的连续堆栈。他们所做的是在每次函数调用时从堆中分配堆栈帧。每个函数调用的成本有所上升;如果函数通常很复杂,或者语言是解释性的,那么这个额外的成本是微不足道的。 (也可以在程序调用图中确定调用DAG并分配一个堆段来覆盖整个DAG;这样就可以获得堆分配和调用DAG内所有调用的经典大堆函数调用的速度。) / p>

对堆栈帧使用堆分配有几个原因:

1)如果程序根据它正在解决的特定问题进行深度递归, 由于所需的尺寸未知,因此很难提前预先分配“大堆”区域。可以笨拙地安排函数调用以检查是否有足够的堆栈,如果没有,重新分配更大的块,复制旧堆栈并重新调整所有指针进入堆栈;这太尴尬了,我不知道任何实现。 分配堆栈帧意味着应用程序永远不必说对不起,直到有 字面上没有可分配的内存。

2)程序分叉子任务。每个子任务都需要自己的堆栈,因此不能使用提供的一个“大堆栈”。因此,需要为每个子任务分配堆栈。如果你有数千个可能的子任务,你现在可能需要成千上万的“大堆栈”,内存需求突然变得荒谬。分配堆栈帧解决了这个问题。子任务“堆栈”通常会引用父任务来实现词法作用域;作为子任务分叉,创建一个称为“仙人掌堆栈”的“亚洲货盘”树。

3)你的语言有延续。这些要求以某种方式保留当前函数可见的词法范围中的数据以供以后重用。这可以通过复制父堆栈帧,爬上仙人掌堆栈并继续进行来实现。

我实施的PARLANSE编程语言1)和2)。我正在努力3)。

答案 1 :(得分:14)

Stackless Python仍然有一个Python堆栈(虽然它可能有尾调用优化和其他调用框架合并技巧),但它完全脱离了解释器的C堆栈。

Haskell(通常实现)没有调用堆栈;评估基于graph reduction

答案 2 :(得分:5)

http://www.linux-mag.com/cache/7373/1.html有一篇关于语言框架Parrot的文章。 Parrot没有使用堆栈进行调用,本文稍微解释了这个技术。

答案 3 :(得分:4)

在无框架环境中,我或多或少熟悉(图灵机,汇编和Brainfuck),实现自己的堆栈很常见。将语言堆叠在语言中没有任何根本。

在最实用的这些程序集中,您只需选择一个可用的内存区域,将堆栈寄存器设置为指向底部,然后递增或递减以实现推送和弹出。

编辑:我知道有些架构有专门的堆栈,但它们不是必需的。

答案 4 :(得分:3)

本文有一个易于理解的延续说明:http://www.defmacro.org/ramblings/fp.html

Continuations是你可以在基于堆栈的语言中传递给函数的东西,但也可以被语言自己的语义用来使它“无堆栈”。当然堆栈仍然存在,但正如Ira Baxter描述的那样,它不是一个大的连续段。

答案 5 :(得分:3)

叫我古老,但我记得FORTRAN标准和COBOL不支持递归调用,因此不需要堆栈。实际上,我记得没有堆栈的CDC 6000系列机器的实现,如果你试图以递归方式调用子程序,FORTRAN会做一些奇怪的事情。

对于记录,CDC 6000系列指令集使用RJ指令调用子程序而不是调用堆栈。这将当前PC值保存在呼叫目标位置,然后分支到其后的位置。最后,子程序将执行到呼叫目标位置的间接跳转。重新加载了保存的PC,有效地返回给调用者。

显然,这不适用于递归调用。 (我的回忆是,如果您尝试递归,CDC FORTRAN IV编译器会生成损坏的代码...)

答案 6 :(得分:3)

假设您想实现无堆栈C.首先要意识到的是,这不需要堆栈:

a == b

但是,这是吗?

isequal(a, b) { return a == b; }

没有。因为智能编译器会内联对isequal的调用,所以将它们转换为a == b。那么,为什么不直接内联一切呢?当然,你会产生更多的代码,但是如果摆脱​​堆栈对你来说是值得的,那么通过一个小的权衡就很容易。

递归怎么样?没问题。尾递归函数,如:

bang(x) { return x == 1 ? 1 : x * bang(x-1); }

仍然可以内联,因为它实际上只是伪装的for循环:

bang(x) {
    for(int i = x; i >=1; i--) x *= x-1;
    return x;
}

理论上,一个非常聪明的编译器可以为您解决这个问题。但是,一个不那么聪明的人仍然可以将其视为一个转到:

ax = x;
NOTDONE:
if(ax > 1) {
    x = x*(--ax);
    goto NOTDONE;
}

有一种情况需要进行小额交易。这不能内联:

fib(n) { return n <= 2 ? n : fib(n-1) + fib(n-2); }

Stackless C根本无法做到这一点。你放弃了很多吗?并不是的。这是正常的C也无法做得很好的事情。如果你不相信我,只需致电fib(1000),看看你宝贵的电脑会发生什么。

答案 7 :(得分:1)

如果我错了,请随意纠正我,但我认为在堆上为每个函数调用帧分配内存会导致极端的内存抖动。毕竟操作系统必须管理这个内存。我认为避免这种内存颠簸的方法是调用帧的缓存。因此,如果你还需要一个缓存,我们不妨在内存中使它成为一个堆栈。