如何实现延续?

Posted

技术标签:

【中文标题】如何实现延续?【英文标题】:How to implement continuations? 【发布时间】:2010-09-05 14:40:19 【问题描述】:

我正在开发一个用 C 编写的 Scheme 解释器。目前它使用 C 运行时堆栈作为自己的堆栈,这在实现延续方面存在一个小问题。我目前的解决方案是将 C 堆栈手动复制到堆中,然后在需要时将其复制回来。除了不是标准的 C 之外,这个解决方案也不是很理想。

在 C 中实现 Scheme 延续的最简单方法是什么?

【问题讨论】:

【参考方案1】:

Clinger、Hartheimer 和 Ost 的文章 Implementation Strategies for First-Class Continuations 提供了很好的摘要。我建议特别关注 Chez Scheme 的实施。

堆栈复制并不那么复杂,并且有许多易于理解的技术可用于提高性能。使用堆分配的帧也相当简单,但是您需要权衡为不使用显式延续的“正常”情况创建开销。

如果您将输入代码转换为连续传递样式 (CPS),那么您可以完全消除堆栈。然而,虽然 CPS 很优雅,但它在前端增加了另一个处理步骤,并且需要额外的优化来克服某些性能影响。

【讨论】:

【参考方案2】:

我记得读过一篇可能对你有帮助的文章:Cheney on the M.T.A. :-)

我知道的一些Scheme实现,比如SISC,在堆上分配它们的调用帧。

@ollie:如果所有调用帧都在堆上,则不需要进行提升。当然,性能需要权衡:提升时间与分配堆上所有帧所需的开销。也许它应该是解释器中一个可调的运行时参数。 :-P

【讨论】:

【参考方案3】:

如果您是从头开始,您真的应该考虑继续传递样式 (CPS) 转换。

好的来源包括“LISP in small pieces”和Marc Feeley's Scheme in 90 minutes presentation。

【讨论】:

Queinnec 的书 Lisp In Small Pieces可用(至少在 Paracampus 的法语版中)【参考方案4】:

到目前为止,似乎没有提到 Dybvig 的论文。 阅读是一种享受。基于堆的模型 是最容易实现的,但基于堆栈 效率更高。忽略基于字符串的模型。

R。肯特·戴维格。 “方案的三种实施模式”。 http://www.cs.indiana.edu/~dyb/papers/3imp.pdf

还可以查看 ReadScheme.org 上的实施文件。 https://web.archive.org/http://library.readscheme.org/page8.html

摘要如下:

本文介绍了Scheme的三种实现模型 编程语言。第一个是在某些应用中使用的基于堆的模型 迄今为止在大多数方案实施中的形式;第二个是新的 基于堆栈的模型比 执行大多数程序时基于堆的模型;第三个是新的 用于多处理器的基于字符串的模型 方案的实施。

基于堆的模型将几个重要的数据结构分配到一个 堆,包括实际参数列表、绑定环境和调用 帧。

基于堆栈的模型在堆栈上分配这些相同的结构 只要有可能。这导致更少的堆分配,更少的内存 引用,更短的指令序列,更少的垃圾收集, 和更有效地使用内存。

基于字符串的模型直接分配这些结构的版本 程序文本,表示为一串符号。在里面 基于字符串的模型,Scheme 程序被翻译成 FFP 专门为支持 Scheme 而设计的语言。这里面的节目 语言由FFP机器直接执行,a 多处理器减串计算机。

基于堆栈的模型具有立竿见影的实用价值;它是 作者的 Chez Scheme 系统使用的模型,一个高性能的 方案的实施。基于字符串的模型将用于 在 FFP 机器上提供 Scheme 作为 FFP 的高级替代方案 一旦机器实现了。

【讨论】:

【参考方案5】:

除了到目前为止您得到的很好的答案,我推荐 Andrew Appel 的 Compiling with Continuations。它写得很好,虽然不直接与 C 打交道,但它为编译器作者提供了非常好的想法。

Chicken Wiki 也有一些您会觉得非常有趣的页面,例如 internal structure 和 compilation process(其中 CPS 是用一个实际的编译示例来解释的)。

【讨论】:

我非常喜欢阿佩尔的书。一个好处是你可以参考 SML/NJ 编译器的源代码,这是 Appel 在书中描述的过程的一个很好的活生生的例子。【参考方案6】:

您可以查看的示例有:Chicken(Scheme 实现,用 C 语言编写,支持延续); Paul Graham 的On Lisp - 他创建了一个 CPS 转换器来实现 Common Lisp 中的延续子集; Weblocks - 一个基于延续的 Web 框架,它还在 Common Lisp 中实现了有限形式的延续。

【讨论】:

请问您指的是On Lisp的哪一章? 第 20 章是关于 Continuations - 特别是 20.3【参考方案7】:

延续不是问题:您可以使用 CPS 实现具有常规高阶函数的那些。天真的堆栈分配的问题是尾调用永远不会被优化,这意味着你不能计划。

当前将方案的意大利面条堆栈映射到堆栈上的最佳方法是使用蹦床:本质上是额外的基础设施来处理非 C 类调用和过程退出。见Trampolined Style (ps)。

some code 说明了这两个想法。

【讨论】:

【参考方案8】:

传统方式是使用setjmplongjmp,但有一些注意事项。

这是reasonably good explanation

【讨论】:

【参考方案9】:

Continuations 基本上由上下文切换点的堆栈和 CPU 寄存器的保存状态组成。最起码切换的时候不用把整个栈复制到堆里,只需要重定向栈指针即可。

Continuations 是使用纤程轻松实现的。 http://en.wikipedia.org/wiki/Fiber_%28computer_science%29 .唯一需要仔细封装的是参数传递和返回值。

在 Windows 中,光纤是使用 CreateFiber/SwitchToFiber 系列调用完成的。 在符合 Posix 的系统中,可以使用 makecontext/swapcontext 来完成。

boost::coroutine 有一个适用于 C++ 的协同程序的工作实现,可以作为实现的参考点。

【讨论】:

简单实现...需要仔细封装 - 这一段有一定的张力。这个答案将通过一些代码的链接得到改进。【参考方案10】:

正如soegaard 所指出的,主要参考仍然是R. Kent Dybvig. "Three Implementation Models for Scheme"

这个想法是,延续是保持其评估控制堆栈的闭包。从使用call/cc 创建延续的那一刻起,需要控制堆栈才能继续评估。

经常调用延续会导致执行时间很长,并用重复的堆栈填充内存。我写了这个愚蠢的代码来证明,在 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 秒,如果您增加输入数字,它将崩溃。

【讨论】:

【参考方案11】:

改为使用显式堆栈。

【讨论】:

-1:显式堆栈是什么?堆分配的数据结构建模堆栈?堆分配的数据结构建模堆栈使用的历史?与问题的相关性?【参考方案12】:

Patrick 是正确的,唯一可以真正做到这一点的方法是在解释器中使用显式堆栈,并在需要转换为延续时将堆栈的适当段提升到堆中。

这与在支持闭包的语言中支持闭包所需的基本相同(闭包和延续有些相关)。

【讨论】:

但是,为了支持闭包,你不能只做 lambda 提升吗?

以上是关于如何实现延续?的主要内容,如果未能解决你的问题,请参考以下文章

在哪里存储以及如何在客户端维护来自 cosmos db 的延续令牌

聊聊多线程哪一些事儿(task)之 二 延续操作

图解面试题:ConcurrentHashMap是如何保证线程安全的

如何实现 iOS 短视频跨页面的无痕续播?

如何防止任务的同步延续?

如何实现 iOS 短视频跨页面的无痕续播?