Как реализовать продолжения?

Я работаю над интерпретатором схемы, написанным на C. В настоящее время он использует стек среды выполнения C как свой собственный стек, что представляет небольшую проблему с реализацией продолжений. Мое текущее решение - ручное копирование стека C в кучу, а затем копирование его обратно при необходимости. Помимо нестандартного C, это решение вряд ли является идеальным.

Каков самый простой способ реализовать продолжения для Scheme в C?

Ответов (12)

Решение

Я помню, как читал статью, которая может вам помочь: Чейни о MTA :-)

Некоторые известные мне реализации Scheme, такие как SISC , размещают свои кадры вызовов в куче.

@ollie: Вам не нужно выполнять подъем, если все кадры вызовов находятся в куче. Конечно, есть компромисс в производительности: время на подъем по сравнению с накладными расходами, необходимыми для выделения всех кадров в куче. Возможно, это должен быть настраиваемый параметр времени выполнения в интерпретаторе. :-П

Патрик прав, единственный способ действительно сделать это - использовать явный стек в вашем интерпретаторе и поднять соответствующий сегмент стека в кучу, когда вам нужно преобразовать в продолжение.

Это в основном то же самое, что требуется для поддержки замыканий на языках, которые их поддерживают (замыкания и продолжения в некоторой степени связаны).

Вместо этого используйте явный стек.

Похоже, тезис Дибвига до сих пор не упоминается. Читать это одно удовольствие. Модель на основе кучи проще всего реализовать, но на основе стека более эффективно. Игнорируйте строковую модель.

Р. Кент Дибвиг. «Три модели реализации схемы». http://www.cs.indiana.edu/~dyb/papers/3imp.pdf

Также ознакомьтесь с документами по реализации на ReadScheme.org. https://web.archive.org/http://library.readscheme.org/page8.html

Аннотация выглядит следующим образом:

В этой диссертации представлены три модели реализации языка программирования схем. Первая - это модель на основе кучи, используемая в той или иной форме в большинстве реализаций Scheme на сегодняшний день; вторая - это новая модель на основе стека, которая значительно более эффективна, чем модель на основе кучи при выполнении большинства программ; и третий - это новая основанная на строках модель, предназначенная для использования в многопроцессорной реализации Scheme.

Модель на основе кучи выделяет несколько важных структур данных в куче, включая фактические списки параметров, среды привязки и кадры вызовов.

Модель на основе стека по возможности распределяет эти же структуры в стеке. Это приводит к меньшему выделению кучи, меньшему количеству обращений к памяти, более коротким последовательностям инструкций, меньшему количеству сборки мусора и более эффективному использованию памяти.

В строковой модели версии этих структур выделяются прямо в тексте программы, который представлен в виде строки символов. В строковой модели программы Scheme переводятся на язык FFP, специально разработанный для поддержки Scheme. Программы на этом языке напрямую выполняются машиной FFP, многопроцессорным компьютером с сокращением строк.

Модель на основе стека приносит немедленную практическую пользу; это модель, используемая авторской системой Chez Scheme, высокопроизводительной реализацией Scheme. Модель на основе строк будет полезна для предоставления Scheme в качестве высокоуровневой альтернативы FFP на машине FFP, как только машина будет реализована.

Если вы начинаете с нуля, вам действительно стоит обратить внимание на преобразование стиля передачи продолжения (CPS).

Good sources include "LISP in small pieces" and Marc Feeley's Scheme in 90 minutes presentation.

Как уже soegaard отмечалось, основная ссылка остается R. Kent Dybvig. "Three Implementation Models for Scheme" .

Идея в том, что продолжение - это закрытие, которое сохраняет свой стек управления оценкой. Стек управления необходим для продолжения оценки с момента создания продолжения с использованием call/cc .

Часто вызов продолжения увеличивает время выполнения и заполняет память дублированными стеками. Я написал этот глупый код, чтобы доказать, что в MIT-схеме он приводит к сбою схемы,

Код суммирует первые 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 секунды, а если вы увеличите входное число, он выйдет из строя.

Традиционный способ - использовать setjmp и longjmp, хотя есть и предостережения.

Вот достаточно хорошее объяснение

Хорошее резюме доступно в статье Clinger, Hartheimer и Ost « Стратегии реализации для первоклассных продолжений» . В частности, рекомендую посмотреть на реализацию Chez Scheme.

Копирование стека не так сложно, и существует ряд хорошо понятных методов повышения производительности. Использование выделенных в куче фреймов также довольно просто, но вы делаете компромисс, создавая накладные расходы для «нормальной» ситуации, когда вы не используете явные продолжения.

Если вы конвертируете входной код в стиль передачи продолжения (CPS), вы можете полностью отказаться от стека. Однако, хотя CPS элегантен, он добавляет еще один этап обработки во внешний интерфейс и требует дополнительной оптимизации для преодоления определенных последствий для производительности.

Вы можете посмотреть следующие примеры: Chicken (реализация схемы, написанная на C, которая поддерживает продолжения); Paul Graham's On Lisp - где он создает преобразователь CPS для реализации подмножества продолжений в Common Lisp; и Weblocks - веб-фреймворк на основе продолжений, который также реализует ограниченную форму продолжений в Common Lisp.

Проблема не в продолжении: вы можете реализовать их с помощью обычных функций высшего порядка, используя CPS. Проблема с наивным распределением стека заключается в том, что хвостовые вызовы никогда не оптимизируются, а это означает, что вы не можете быть схемой.

Наилучший текущий подход к отображению стека спагетти схемы в стек - использование трамплинов: по сути, дополнительная инфраструктура для обработки не-C-подобных вызовов и выходов из процедур. См. « Батутный стиль» (ps) .

Есть некоторый код, иллюстрирующий обе эти идеи.

Продолжения в основном состоят из сохраненного состояния стека и регистров ЦП в точке переключения контекста. По крайней мере, вам не нужно копировать весь стек в кучу при переключении, вы можете только перенаправить указатель стека.

Продолжение тривиально реализуется с помощью волокон. http://en.wikipedia.org/wiki/Fiber_%28computer_science%29 . Единственное, что требует тщательной инкапсуляции, - это передача параметров и возвращаемые значения.

В Windows волокна выполняются с использованием семейства вызовов CreateFiber / SwitchToFiber. в Posix-совместимых системах это можно сделать с помощью makecontext / swapcontext.

boost :: coroutine имеет рабочую реализацию сопрограмм для C++, которая может служить ориентиром для реализации.

Помимо хороших ответов, которые у вас есть, я рекомендую Эндрю Аппеля " Компиляция с продолжениями" . Он очень хорошо написан и не имеет прямого отношения к C, но является источником действительно хороших идей для разработчиков компиляторов.

В Chicken Wiki также есть страницы, которые вы найдете очень интересными, такие как внутренняя структура и процесс компиляции (где CPS объясняется на реальном примере компиляции).