我正在使用C编写的Scheme解释器。目前它使用C运行时堆栈作为自己的堆栈,这实现了实现continuation的一个小问题。我目前的解决方案是手动将C堆栈复制到堆中,然后在需要时将其复制回来。除了不是标准C之外,这个解决方案并不理想。
在C中实现Scheme的延续的最简单方法是什么?
答案 0 :(得分:28)
Clinger,Hartheimer和Ost的文章Implementation Strategies for First-Class Continuations中提供了一个很好的摘要。我建议特别关注Chez Scheme的实施。
堆栈复制并不复杂,并且有许多易于理解的技术可用于提高性能。使用堆分配的帧也相当简单,但是您需要权衡为“正常”情况创建开销,而不使用显式延续。
如果您将输入代码转换为延续传递样式(CPS),那么您可以完全消除堆栈。然而,虽然CPS很优雅,但它在前端增加了另一个处理步骤,需要进行额外的优化以克服某些性能影响。
答案 1 :(得分:17)
我记得读过一篇可能对您有所帮助的文章: Cheney on the M.T.A. : - )
我知道的Scheme的一些实现,例如SISC,在堆上分配它们的调用帧。
@ollie:如果所有的调用框都在堆上,则无需进行吊装。当然还有性能上的权衡:提升的时间,以及在堆上分配所有帧所需的开销。也许它应该是解释器中的可调运行时参数。 :-P
答案 2 :(得分:12)
如果你是从头开始,你真的应该选择继续传递风格(CPS)转换。
良好的消息来源包括“小块LISP”和Marc Feeley's Scheme in 90 minutes presentation。
答案 3 :(得分:9)
到目前为止,Dybvig的论文似乎未被提及。 阅读是一种乐趣。基于堆的模型 是最容易实现的,但基于堆栈 效率更高。忽略基于字符串的模型。
R上。 Kent Dybvig。 “计划的三种实施模式”。 http://www.cs.indiana.edu/~dyb/papers/3imp.pdf
另请参阅ReadScheme.org上的实施文件。 http://library.readscheme.org/page8.html
摘要如下:
本论文介绍了该方案的三种实施模型 编程语言。第一种是在某些情况下使用的基于堆的模型 到目前为止大多数Scheme实现中的形式;第二个是新的 基于堆栈的模型比它更有效 执行大多数程序时基于堆的模型;第三是新的 用于多处理器的基于字符串的模型 实施计划。
基于堆的模型在a中分配了几个重要的数据结构 堆,包括实际参数列表,绑定环境和调用 帧。
基于堆栈的模型在堆栈上分配这些相同的结构 只要有可能。这样可以减少堆分配,减少内存 引用,更短的指令序列,更少的垃圾收集, 并且更有效地使用记忆。
基于字符串的模型直接分配这些结构的版本 程序文本,表示为一串符号。在里面 基于字符串的模型,Scheme程序被翻译成FFP 专为支持Scheme而设计的语言。这个程序 语言由FFP机器直接执行,a 多处理器字符串缩减计算机。
基于堆栈的模型具有直接的实用性;它是 作者使用的模型Chez Scheme系统,具有高性能 实施计划。基于字符串的模型将非常有用 提供Scheme作为FFP机器上FFP的高级替代品 一旦机器实现了。
答案 4 :(得分:8)
除了你到目前为止得到的好答案之外,我还推荐Andrew Appel的Compiling with Continuations。它编写得很好,虽然没有直接处理C,但它是编译器编写者非常好的想法的来源。
Chicken Wiki还有一些你会发现非常有趣的页面,例如internal structure和compilation process(其中CPS用一个实际的编译示例来解释)。
答案 5 :(得分:7)
传统方式是使用setjmp
和longjmp
,但有一些警告。
答案 6 :(得分:7)
您可以看到的示例有:Chicken(一种Scheme实现,用C语言编写,支持continuation);保罗格雷厄姆的On Lisp - 在那里他创建了一个CPS变换器来实现Common Lisp中的一系列延续;和Weblocks - 基于继续的Web框架,它还在Common Lisp中实现了有限形式的延续。
答案 7 :(得分:7)
Continuations不是问题:您可以使用CPS实现具有常规高阶函数的那些。天真堆栈分配的问题是尾部调用永远不会被优化,这意味着你不能成为方案。
将方案的spaghetti堆栈映射到堆栈的最新方法是使用trampolines:基本上是额外的基础设施来处理非C类调用和退出程序。请参阅Trampolined Style (ps)。
有some code说明了这两种想法。
答案 8 :(得分:5)
Continuations基本上包括堆栈的保存状态和上下文切换点的CPU寄存器。至少你不必在切换时将整个堆栈复制到堆中,你只能重定向堆栈指针。
使用光纤可以简单地实现连续性。 http://en.wikipedia.org/wiki/Fiber_%28computer_science%29 。唯一需要仔细封装的是参数传递和返回值。
在Windows中,光纤使用CreateFiber / SwitchToFiber系列调用完成。 在符合Posix的系统中,可以使用makecontext / swapcontext来完成。
boost :: coroutine有一个C ++协程的工作实现,可以作为实现的参考点。
答案 9 :(得分:1)
改为使用显式堆栈。
答案 10 :(得分:1)
Patrick是正确的,唯一可以做到这一点的方法是在解释器中使用显式堆栈,并在需要转换为延续时将相应的堆栈段提升到堆中。
这与支持它们的语言中的闭包所需的基本相同(闭包和延续有些相关)。
答案 11 :(得分:1)
正如soegaard
所指出的那样,主要参考文献仍为this one
这个想法是,延续是一个保持其评估控制堆栈的闭包。需要控制堆栈才能从使用call/cc
创建延续的那一刻开始继续评估。
通常调用continuation会占用很长时间,并使用重复的堆栈填充内存。我写了这个愚蠢的代码来证明,在mit-scheme中它会导致程序崩溃,
代码汇总前1000个数字1+2+3+...+1000
。
(call-with-current-continuation
(lambda (break)
((lambda (s) (s s 1000 break))
(lambda (s n cc)
(if (= 0 n)
(cc 0)
(+ n
;; non-tail-recursive,
;; the stack grows at each recursive call
(call-with-current-continuation
(lambda (__)
(s s (- n 1) __)))))))))
如果您从1000切换到100 000,代码将花费2秒,如果您增加输入数字,它将崩溃。