Які альтернативи використанню стека для представлення семантики виклику функції?


19

Всі ми знаємо і любимо, що виклики функцій зазвичай реалізуються за допомогою стека; є кадри, зворотні адреси, параметри, вся партія.

Однак стек - це деталь реалізації: виклики конвенцій можуть робити різні речі (наприклад, регістри швидкого виклику x86 (деякі) регістри, MIPS та послідовники використовують вікна реєстру тощо), а оптимізація може робити навіть інші речі (вбудовування, опущення покажчика кадру, оптимізація хвостових викликів ..).

Звичайно, наявність зручної інструкції щодо стека на багатьох машинах (таких машин, як JVM та CLR, а також реальних машин, таких як x86, з їх PUSH / POP тощо) дозволяє зручно використовувати її для викликів функцій, але в деяких випадках це можливо програмувати таким чином, коли стек викликів не потрібен (я тут думаю про стиль продовження передачі або Актори в системі передачі повідомлень)

Отже, я починав задаватися питанням: чи можна реалізувати семантику викликів функцій без стека, а ще краще, використовуючи іншу структуру даних (черга, можливо, або асоціативна карта?)
Звичайно, я розумію, що стек дуже зручно (є причина, чому це всюдисуще), але я нещодавно натрапив на реалізацію, яка змусила мене замислитися ..

Хтось із вас знає, чи це колись робилося на будь-якій мові / машині / віртуальній машині, і якщо так, то які яскраві відмінності та недоліки?

EDIT: Мої відчуття кишечника полягають у тому, що різні підходи до обчислення можуть використовувати різні структури даних. Наприклад, лямбда-числення не засноване на стеці (ідея застосування функції охоплюється скороченнями), але я дивився на реальну мову / машину / приклад. Ось чому я прошу ...


Clean використовує машину для перезапису графіків та графіків, яка, в свою чергу, реалізується за допомогою трискладної машини, але з іншим змістом, ніж зазвичай.

Для віртуальних машин можна використовувати пов'язаний список. Кожен вузол списку - це кадр. Оскільки апаратний стек використовується VM, це дозволяє кадрам існувати на купі без накладних витрат realloc().
shawnhcorey

Відповіді:


19

Залежно від мови, можливо, не доведеться використовувати стек викликів. Стеки дзвінків необхідні лише на мовах, які дозволяють рекурсію або взаємну рекурсію. Якщо мова не дозволяє рекурсії, то в будь-який момент може бути активним лише один виклик будь-якої процедури, і локальні змінні для цієї процедури можуть бути статично виділені. Такі мови повинні передбачати зміни контексту, обробку переривань, але це все ще не вимагає складання.

Зверніться до FORTRAN IV (і новіших версій) та ранніх версій COBOL для прикладів мов, які не потребують стеків викликів.

Зверніться до Control Data 6600 (і більш ранніх машин Control Data) для прикладу дуже успішного раннього суперкомп'ютера, який не забезпечував прямої апаратної підтримки для стеків викликів. Зверніться до PDP-8 для прикладу дуже успішного раннього мінікомп'ютера, який не підтримував стеки викликів.

Наскільки я знаю, машини з стеком Burroughs B5000 були першими машинами з апаратними стеками викликів. Машини B5000 були розроблені з нуля для запуску ALGOL, що вимагало рекурсії. Вони також мали одну з перших архітектур на основі дескрипторів, яка заклала основи для архітектурних можливостей.

Наскільки мені відомо, саме PDP-6 (який перетворився на DEC-10) популяризував апаратне забезпечення стека викликів, коли спільнота хакерів на MIT взяла доставку і виявила, що операція PUSHJ (Push Return Address and Jump) дозволило зменшити процедуру десяткового друку з 50 інструкцій до 10.

Найбільш основна семантика виклику функції мовою, яка дозволяє рекурсії, вимагає можливостей, які добре відповідають стеку. Якщо це все, що вам потрібно, то базовий стек - це хороша, проста відповідність. Якщо вам потрібно більше, то ваша структура даних повинна робити більше.

Найкращий приклад необхідності мені більше, ніж у мене, - це "продовження", можливість призупинити обчислення в середині, зберегти його як застиглий міхур стану і знову випустити його згодом, можливо, багато разів. Продовження стали популярними в діалекті схеми LISP, як спосіб впровадження, серед іншого, помилок виходу. Продовження вимагає можливості зробити знімок поточного середовища виконання та відтворити його пізніше, а стек для цього дещо незручний.

"Структура та інтерпретація комп'ютерних програм" Абельсона та Суссмана детально розглядає продовження.


2
Це було чудове історичне розуміння, дякую! Коли я задавав своє запитання, я мав на увазі продовження, особливо стиль продовження передачі (CPS). У цьому випадку стек є не тільки незручним, але, ймовірно, і не потрібним: вам не потрібно пам’ятати, куди повертатися, ви надаєте пункт, де продовжувати виконання. Мені було цікаво, чи є інші підходи без стоп, де загальні, і ви дали дуже хороші, про яких я не знав.
Лоренцо Дематте

Трохи пов'язані: ви правильно вказали, "якщо мова не дозволяє рекурсії". Що щодо мови з рекурсією, зокрема функцій, які не є рекурсивними? Чи потрібен їм стек "по дизайну"?
Лоренцо Дематте

"Стеки дзвінків необхідні лише на мовах, які дозволяють рекурсію або взаємну рекурсію" - ні. Якщо функцію можна викликати з більш ніж одного місця (наприклад, обидва fooта barможуть дзвонити baz), функція повинна знати, до чого повернутися. Якщо ви вкладете цю інформацію "до кого повернутися", тоді ви закінчите стек. Не має значення, як ви його називаєте, чи підтримується він апаратним забезпеченням центрального процесора або тим, що емулюєте у програмному забезпеченні (або навіть якщо його пов'язаний список статично виділених записів), це все ще стек.
Брендан

@Brendan не обов'язково (принаймні, у цьому і полягає вся мета мого запитання). "Куди повернутися до" або краще ", куди йти далі", чи повинен це бути стек, тобто структура LIFO? Чи може це бути дерево, карта, черга чи щось інше?
Лоренцо Дематте

Наприклад, мої відчуття кишечника полягають у тому, що CPS просто потребує дерева, але я не впевнений, і я не знаю, куди дивитись. Ось чому я прошу ..
Лоренцо Дематте

6

Неможливо реалізувати семантику виклику функцій без використання якогось стека. Можна грати лише в ігри в слова (наприклад, використовувати іншу назву для цього, наприклад, "FILO return buffer").

Можна використовувати те, що не реалізує семантику виклику функцій (наприклад, стиль передачі продовження, актори), а потім над ним будувати семантику виклику функцій; але це означає додавати якусь структуру даних, щоб відстежувати, куди передається управління, коли функція повертається, і ця структура даних буде типом стеку (або стека з іншим іменем / описом).

Уявіть, що у вас є багато функцій, які можуть всі телефонувати один одному. Під час виконання кожна функція повинна знати, куди слід повернутися, коли функція закінчується. Якщо firstдзвінки, secondто у вас є:

second returns to somewhere in first

Потім, якщо у вас є secondдзвінки third:

third returns to somewhere in second
second returns to somewhere in first

Потім, якщо у вас є thirdдзвінки fourth:

fourth returns to somewhere in third
third returns to somewhere in second
second returns to somewhere in first

Як називається кожна функція, більше інформації "куди повернути" повинна зберігатися десь.

Якщо функція повертається, то її інформація "куди повернути" використовується і більше не потрібна. Наприклад, якщо fourthповертається десь назад, thirdто кількість інформації "куди повернутися" стане такою:

third returns to somewhere in second
second returns to somewhere in first

В основному; "Семантика виклику функції" означає, що:

  • у вас повинна бути інформація "куди повернути"
  • кількість інформації зростає у міру виклику функцій та зменшується, коли функції повертаються
  • перший фрагмент інформації "куди повернути" зберігається буде останнім фрагментом інформації "куди повернути"

Це описує буфер FILO / LIFO або стек.

Якщо ви намагаєтесь використовувати тип дерева, то кожен вузол на дереві ніколи не матиме більше однієї дитини. Примітка: вузол з кількома дітьми може статися лише в тому випадку, якщо функція викликає одночасно 2 або більше функцій , що вимагає певної одночасності (наприклад, потоки, fork () тощо), і це не було б "семантикою виклику функції". Якщо кожен вузол на дереві ніколи не матиме більше однієї дитини; тоді це "дерево" буде використовуватися лише як буфер FILO / LIFO або стек; і тому, що він використовується лише як буфер FILO / LIFO або стек, справедливо стверджувати, що "дерево" є стеком (і єдиною різницею є слова в ігри та / або деталі реалізації).

Це ж стосується будь-якої іншої структури даних, яка, можливо, може бути використана для реалізації "семантики виклику функцій" - вона буде використовуватися як стек (і єдиною різницею є слова в ігри та / або деталі реалізації); якщо це не порушує "семантику виклику функції". Примітка. Я б наводив приклади для інших структур даних, якби міг, але я не можу придумати будь-яку іншу структуру, яка є мало правдоподібною.

Звичайно, як стек реалізований - це деталь реалізації. Це може бути область пам’яті (де ви відстежуєте «поточну вершину стека»), це може бути якийсь пов'язаний список (де ви відстежуєте «поточний запис у списку»), або він може бути реалізований у деяких інший шлях. Також не має значення, чи має апаратне забезпечення вбудовану підтримку чи ні.

Примітка: Якщо в будь-який момент може бути активним лише один виклик будь-якої процедури; тоді ви можете статично виділити простір для інформації "куди повернутися до". Це все ще стек (наприклад, пов'язаний список статично виділених записів, використовуваних способом FILO / LIFO).

Також зауважте, що є деякі речі, які не відповідають "семантиці виклику функції". Ці речі включають "потенційно дуже різну семантику" (наприклад, продовження проходження, акторська модель); а також включає поширені розширення на "функцію семантики викликів", як паралельність (потоки, волокна, що завгодно), setjmp/ longjmp, обробка виключень тощо.


За визначенням, стек - це колекція LIFO: Last in, first out. Черга - це колекція FIFO.
Джон Р. Стром

Так чи є стек єдиною прийнятною структурою даних? Якщо так, то чому?
Лоренцо Дематте

@ JohnR.Strohm: Виправлено :-)
Брендан

1
Для мов без рекурсії (прямої або мутальної) можна статично виділити для кожного методу змінну, яка буде визначати місце, з якого цей метод востаннє викликався. Якщо лінкер знає про такі речі, він може виділяти такі змінні таким чином, що буде не гірше того, що зробив би стек, якби насправді був здійснений кожен статично можливий шлях виконання .
supercat

4

Іграшкова мова конкатенації XY використовує чергу викликів та стек даних для виконання.

Кожен крок обчислення просто включає в себе декінгування наступного слова, яке потрібно виконати, а у випадку вбудованих файлів - подачу його внутрішньої функції стеком даних і чергою викликів як аргументів, або за допомогою userdefs, висунення слів, що складають його на передню частину черги.

Отже, якщо у нас є функція подвоїти верхній елемент:

; double dup + ;
// defines 'double' to be composed of 'dup' followed by '+'
// dup duplicates the top element of the data stack
// + pops the top two elements and push their sum

Тоді функції складання +і dupмають такі типи підписів стека / черги:

// X is arbitraty stack, Y is arbitrary queue, ^ is concatenation
+      [X^a^b Y] -> [X^(a + b) Y]
dup    [X^a Y] -> [X^a^a Y]

Як не парадоксально, doubleце виглядатиме так:

double [X Y] -> [X dup^+^Y]

Так що в певному сенсі XY є бездоганним.


Нічого, дякую! Я
погляну

1
@ Карл Дамгаард Асмуссен "штовхає слова, складаючи їх на передню частину черги", "штовхаючи фронт" Це не стек?

@ guesttttttt222222222 не дуже. Стойка дзвінків зберігає покажчики повернення, і коли функція повертається, стопку дзвінка вискакує. Черга виконання зберігає лише покажчики на функції, а при виконанні наступної функції вона розширюється до її визначення та висувається на передню частину черги. У XY черга виконання - це фактично задача, оскільки є операції, які працюють і на звороті черги виконання.
Карл Дамгаард Асмуссен
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.