Большинство архитектур, которые я видел, используют стек вызовов для сохранения / восстановления контекста перед вызовами функций. Это настолько распространенная парадигма, что операции push и pop встроены в большинство процессоров. Существуют ли системы, которые работают без стека? Если да, то как они работают и для чего они используются?
computer-architecture
ConditionRacer
источник
источник
Ответы:
(Несколько) популярной альтернативой стеку вызовов являются продолжения .
Например, виртуальная машина Parrot основана на продолжении. Это полностью без стеков: данные хранятся в регистрах (например, Dalvik или LuaVM, Parrot на основе регистров), а поток управления представлен продолжениями (в отличие от Dalvik или LuaVM, которые имеют стек вызовов).
Другой популярной структурой данных, обычно используемой виртуальными машинами Smalltalk и Lisp, является стек спагетти, который напоминает сеть стеков.
Как указал @rwong , стиль передачи продолжения является альтернативой стеку вызовов. Программы, написанные (или преобразованные) в стиль передачи продолжения, никогда не возвращаются, поэтому нет необходимости в стеке.
Отвечая на ваш вопрос с другой точки зрения: возможно иметь стек вызовов без отдельного стека, выделяя кадры стека в куче. Некоторые реализации Lisp и Scheme делают это.
источник
В старые времена у процессоров не было стековых инструкций, а языки программирования не поддерживали рекурсию. Со временем все больше и больше языков выбирают поддержку рекурсии, и аппаратные средства следуют за пакетом с возможностями выделения стеков. Эта поддержка значительно варьировалась на протяжении многих лет для разных процессоров. Некоторые процессоры приняли регистры стека и / или указатели стека; некоторые принятые инструкции, которые бы выполняли распределение кадров стека в одной инструкции.
Поскольку процессоры развивались с одноуровневым, а затем многоуровневым кэшем, одним из важнейших преимуществ стека является его локальность. Вершина стека почти всегда находится в кеше. Всякий раз, когда вы можете сделать что-то с высокой частотой обращений к кешу, вы на правильном пути с современными процессорами. Кэш, примененный к стеку, означает, что локальные переменные, параметры и т. Д. Почти всегда находятся в кеше и имеют наивысший уровень производительности.
Короче говоря, использование стека развивалось как в аппаратном, так и в программном обеспечении. Существуют и другие модели (например, вычисление потоков данных в течение длительного периода), однако локальность стека делает его работу действительно хорошей. Кроме того, процедурный код - это как раз то, что процессоры хотят для производительности: одна инструкция говорит ему, что делать после другой. Когда инструкции имеют линейный порядок, процессор сильно замедляется, по крайней мере, пока, так как мы не выяснили, как сделать произвольный доступ таким же быстрым, как последовательный доступ. (Кстати, есть похожие проблемы на каждом уровне памяти, от кеша до основной памяти, до диска ...)
Между продемонстрированной производительностью последовательных инструкций доступа и выгодным поведением кэширования стека вызовов мы имеем, по крайней мере в настоящее время, выигрышную модель производительности.
(Мы могли бы также добавить изменчивость структур данных ...)
Это не означает, что другие модели программирования не могут работать, особенно когда они могут быть преобразованы в последовательные инструкции и модель стека вызовов современного оборудования. Но у моделей, которые поддерживают аппаратное обеспечение, есть явное преимущество. Тем не менее, вещи не всегда остаются неизменными, поэтому мы можем увидеть изменения в будущем, так как различные технологии памяти и транзисторов допускают больше параллелизма. Это всегда шутка между языками программирования и аппаратными возможностями, так что посмотрим!
источник
TL; DR
Остальная часть этого ответа - случайная коллекция мыслей и анекдотов, и поэтому несколько дезорганизована.
Стек, который вы описали (как механизм вызова функции), специфичен для императивного программирования.
Ниже императивного программирования вы найдете машинный код. Машинный код может эмулировать стек вызовов, выполняя небольшую последовательность инструкций.
Ниже машинного кода вы найдете оборудование, ответственное за выполнение программного обеспечения. Хотя современный микропроцессор слишком сложен, чтобы описывать его здесь, можно представить, что существует очень простая конструкция, которая медленная, но все еще способна выполнять тот же машинный код. Такой простой дизайн будет использовать основные элементы цифровой логики:
Следующие обсуждения содержали множество примеров альтернативных способов структурирования императивных программ.
Структура такой программы будет выглядеть так:
Этот стиль будет подходящим для микроконтроллеров, то есть для тех, кто видит программное обеспечение как дополнение к функциям аппаратного обеспечения.
источник
Нет, не обязательно
Прочитать старую статью Аппеля Сбор мусора может быть быстрее, чем распределение стеков . Он использует стиль передачи продолжения и показывает реализацию без стеков.
Также обратите внимание, что старые компьютерные архитектуры (например, IBM / 360 ) не имели никакого аппаратного регистра стека. Но ОС и компилятор зарезервировали регистр для указателя стека по соглашению (связанному с соглашениями о вызовах ), чтобы они могли иметь программный стек вызовов .
В принципе, весь программный компилятор и оптимизатор C могут обнаружить случай (несколько распространенный для встроенных систем), когда граф вызовов статически известен и без какой-либо рекурсии (или указателей на функции). В такой системе каждая функция могла сохранять свой обратный адрес в фиксированном статическом местоположении (и именно так Fortran77 работал на компьютерах эпохи 1970 года).
В наши дни процессоры также имеют стеки вызовов (и машинные инструкции вызова и возврата), осведомленные о кэш- памяти ЦП .
источник
SUBROUTINE
иFUNCTION
. Вы правы для более ранних версий, хотя (FORTRAN-IV и, возможно, WATFIV).TR
иTRT
.Пока у вас есть хорошие ответы; позвольте мне привести вам непрактичный, но очень образовательный пример того, как вы могли бы создать язык без понятия стеков или «потока управления» вообще. Вот программа, которая определяет факториалы:
Мы помещаем эту программу в строку, и мы оцениваем программу по текстовой подстановке. Поэтому, когда мы оцениваем
f(3)
, мы выполняем поиск и заменяем на 3 для i, например так:Отлично. Теперь мы выполняем другую текстовую подстановку: мы видим, что условие «if» ложно, и выполняем замену другой строки, создавая программу:
Теперь мы заменим еще одну строку для всех подвыражений, содержащих константы:
И вы видите, как это происходит; Я не буду вдаваться в подробности. Мы могли бы продолжать выполнять серию подстановок строк, пока мы не приступим к работе
let x = 6
и не закончим.Мы традиционно используем стек для локальных переменных и информации о продолжении; помните, что стек не говорит вам, откуда вы пришли, он говорит вам, куда вы идете дальше с этим возвращаемым значением в руке.
В модели программирования подстановки строк в стеке нет «локальных переменных»; формальные параметры заменяются на их значения, когда функция применяется к ее аргументу, а не помещается в таблицу поиска в стеке. И здесь нет «идти куда-то дальше», потому что оценка программы просто применяет простые правила для подстановки строк для создания другой, но эквивалентной программы.
Теперь, конечно, на самом деле выполнение подстановок строк - это, вероятно, не тот путь. Но языки программирования, которые поддерживают «эквациональные рассуждения» (такие как Haskell), логически используют эту технику.
источник
Со времени публикации Parnas в 1972 г. « О критериях, которые будут использоваться при разложении систем на модули », было разумно принято, что информация, скрывающаяся в программном обеспечении, - это хорошо. Это следует из долгих дебатов 60-х годов о структурной декомпозиции и модульном программировании.
модульность
Необходимый результат отношений черного ящика между модулями, реализованными различными группами в любой многопоточной системе, требует наличия механизма, обеспечивающего возможность повторного входа, и средства отслеживания динамического графа вызовов системы. Контролируемый поток выполнения должен проходить как в, так и из нескольких модулей.
Динамический обзор
Как только лексическая область видимости недостаточна для отслеживания динамического поведения, для отслеживания разницы требуется некоторая бухгалтерия во время выполнения.
Поскольку любой поток (по определению) имеет только один текущий указатель инструкций, LIFO-стек подходит для отслеживания каждого вызова.
Исключения
Таким образом, хотя модель продолжения не поддерживает структуру данных явно для стека, все еще существует вложенный вызов модулей, который необходимо где-то поддерживать!
Даже декларативные языки либо поддерживают историю оценки, либо наоборот выравнивают план выполнения по соображениям производительности и поддерживают прогресс каким-либо другим способом.
Структура с бесконечным циклом, идентифицируемая rwong , широко распространена в высоконадежных приложениях со статическим планированием, которые запрещают многие общие структуры программирования, но требуют, чтобы все приложение рассматривалось как белый ящик без существенной сокрытия информации.
Несколько параллельных бесконечных циклов не требуют какой-либо структуры для хранения адресов возврата, поскольку они не вызывают функции, что делает вопрос спорным. Если они обмениваются данными с помощью общих переменных, то они могут легко выродиться в устаревшие аналоги обратного адреса в стиле Фортрана.
источник
Все старые мэйнфреймы (IBM System / 360) вообще не имели понятия о стеке. Например, в 260 параметры создавались в фиксированном месте в памяти, а когда вызывалась подпрограмма, она вызывалась с
R1
указанием на блок параметров иR14
с адресом возврата. Вызванная подпрограмма, если она хочет вызвать другую подпрограмму, должна была бы быть сохраненаR14
в известном месте перед выполнением этого вызова.Это гораздо надежнее, чем стек, потому что все может храниться в фиксированных местах памяти, установленных во время компиляции, и на 100% гарантируется, что процессы никогда не исчерпают стек. В настоящее время нам не нужно заниматься «выделением 1 МБ и скрестить пальцы».
Рекурсивные вызовы подпрограмм были разрешены в PL / I, указав ключевое слово
RECURSIVE
. Они означали, что память, используемая подпрограммой, была динамически, а не статически распределена. Но рекурсивные вызовы были такими же редкими, как и сейчас.Работа без стеков также значительно облегчает массовую многопоточность, поэтому часто предпринимаются попытки сделать современные языки бесшумными. Например, нет никаких причин, по которым компилятор C ++ не может быть модифицирован как внутренняя часть для использования динамически выделяемой памяти, а не стеков.
источник