Чи є стеки єдиним розумним способом структурування програм?


74

Більшість архітектур, яких я бачив, покладаються на стек викликів для збереження / відновлення контексту перед викликами функцій. Це така загальна парадигма, що операції push та pop вбудовані в більшість процесорів. Чи є системи, які працюють без стека? Якщо так, то як вони працюють, і для чого вони використовуються?


5
Зважаючи на те, як очікується, що функції поводяться на мовах, подібних до С (тобто ви можете вкладати дзвінки настільки глибоко, як вам подобається, і повертатись назад у зворотному порядку), мені незрозуміло, як інакше можна було б реалізувати виклики функцій, не будучи неймовірно неефективна. Ви можете, наприклад, змусити програміста використовувати стиль проходження продовження або якусь химерну форму програмування, але, здається, ніхто з якихось причин насправді не працює з CPS на низькому рівні.
Кевін

5
GLSL працює без стека (як і інші мови у певній дужці). Він просто відключає рекурсивні дзвінки.
Левшенко

3
Ви також можете заглянути в регістри вікон , які використовуються деякими архітектурами RISC.
Марк Бут

2
@Kevin: "Ранні компілятори FORTRAN не підтримували рекурсії в підпрограмах. Ранні комп'ютерні архітектури не підтримували жодної концепції стека, і коли вони безпосередньо підтримували виклики підпрограми, місце повернення часто зберігалося в одному фіксованому місці поруч із кодом підпрограми, що робить не дозволяють повторно викликати підпрограму до повернення попереднього виклику підпрограми. Хоча це не визначено у Fortran 77, багато компіляторів F77 підтримують рекурсію як опцію, хоча вона стала стандартом у Fortran 90. " en.wikipedia.org/wiki/Fortran#FORTRAN_II
Mooing Duck

3
Мікроконтролер P8X32A ("Propeller") не має поняття стека в стандартній мові збірки (PASM). Інструкції, відповідальні за стрибки, також самостійно змінюють інструкцію повернення в оперативній пам’яті, щоб визначити, куди повернутися, - яку можна вибрати довільно. Цікаво, що мова "Spin" (інтерпретована мова високого рівня, що працює на одній мікросхемі) має традиційну семантику стека.
Wossname

Відповіді:


50

Деякою популярною альтернативою стеку викликів є продовження .

Наприклад, папуга VM заснована на продовженні. Це повністю безрезультатно: дані зберігаються в регістрах (наприклад, Dalvik або LuaVM, Parrot заснований на регістрі), а потік управління представлений з продовженнями (на відміну від Dalvik або LuaVM, які мають стек викликів).

Інша популярна структура даних, яку зазвичай використовують VM Smalltalk та Lisp, - це стек спагетті, який схожий на мережу стеків.

Як зазначав @rwong , стиль передачі продовження є альтернативою стеку викликів. Програми, написані у (або перетвореному) стилі продовження передачі, ніколи не повертаються, тому немає необхідності в стеці.

Відповідаючи на ваше питання з іншої точки зору: можна мати стек викликів, не маючи окремого стека, виділивши кадри стека на купу. Деякі реалізації Lisp та Scheme роблять це.


4
Це залежить від вашого визначення стека. Я не впевнений, що наявність пов'язаного списку (або масиву покажчиків на або ...) фреймів стека - це стільки "не стека", скільки "іншого представлення стека"; і програми на мовах CPS (на практиці), як правило, створюють те, що ефективно пов'язані списки продовжень, дуже схожих на стеки (якщо ви цього не зробили, ви можете перевірити GHC, який висуває те, що воно називає "продовження", на лінійний стек для ефективності).
Джонатан У ролях

6
" Програми, написані в (або перетворені) в стилі продовження-проходження ніколи не повертаються " ... звучать зловісно.
Роб Пенридж

5
@RobPenridge: це трохи загадковим, я згоден. CPS означає, що замість повернення функції беруть додатковий аргумент ще одну функцію для виклику, коли виконується їх робота. Отже, коли ви викликаєте функцію і у вас є якась інша робота, яку вам потрібно виконати після виклику функції, замість того, щоб чекати, коли функція повернеться, а потім продовжите свою роботу, ви завершуєте роботу, що залишилася ("продовження" ) у функцію та передайте цю функцію як додатковий аргумент. Функція, яку ви викликали, викликає цю функцію замість повернення тощо, і так далі. Жодна функція ніколи не повертається, вона просто
Jörg W Mittag

3
… Викликає наступну функцію. Тому вам не потрібен стек викликів, тому що вам ніколи не потрібно повертатися та відновлювати стан зв’язування раніше викликаної функції. Замість того, щоб переносити минуле стан, щоб ви могли повернутися до нього, ви переносите майбутній стан, якщо хочете.
Йорг W Міттаг

1
@jcast: Визначальною особливістю стеку є IMO, що ви можете отримати доступ лише до самого верхнього елемента. Список продовжень, OTOH, надасть вам доступ до всіх продовжень, а не лише до найвищого стека. Наприклад, якщо у вас є виняткові відновлювальні винятки в стилі Smalltalk, вам, можливо, потрібно пройти стек, не висуваючи його. І продовження мови, хоча все ще хочеться зберегти звичне уявлення про стек викликів, призводить до стеків спагетті, що в основному є деревом стеків, де продовження "розщеплює" стек.
Йорг W Міттаг

36

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

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

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

Між продемонстрованою виконанням інструкцій послідовного доступу та вигідною кешируючою поведінкою стеку викликів у нас, принаймні, на даний момент, виграшна модель продуктивності.

(Ми можемо також кинути змінні структури даних також у твори ...)

Це не означає, що інші моделі програмування не можуть працювати, особливо коли вони можуть бути переведені в послідовні інструкції та модель стека викликів сучасного обладнання. Але є чітка перевага для моделей, які підтримують обладнання, де є обладнання. Однак речі не завжди залишаються однаковими, тому ми могли побачити зміни в майбутньому, оскільки різні технології пам'яті та транзистори дозволяють отримати більше паралелізму. Це завжди перепад між мовами програмування та апаратними можливостями, тому ми побачимо!


9
Насправді, у графічних процесорів все ще немає стеків. Вам заборонено повторювати в GLSL / SPIR-V / OpenCL (не впевнений у HLSL, але, мабуть, я не бачу причини, чому це було б інакше). Те, як вони насправді обробляють функцію, називають "стеками", використовуючи абсурдно велику кількість регістрів.
LinearZoetrope

@Jsor: Це значною мірою детально реалізує, як видно з архітектури SPARC. Як і у вашого графічного процесора, SPARC має величезний набір реєстрів, але він унікальний тим, що має розсувне вікно, яке на обгортці розсипає дуже старі регістри до стека в ОЗУ. Так що це справді гібрид між двома моделями. І SPARC не вказав, скільки саме фізичних регістрів було, а скільки було велике вікно реєстру, тому різні реалізації могли лежати в будь-якому місці в цій шкалі від "величезної кількості регістрів" до "достатньо для одного вікна, при кожному виклику функції розсипати прямо на стек "
MSalters

Нижньою стороною моделі стека викликів є те, що за масивом та / або переповненням адреси слід спостерігати дуже уважно, оскільки самостійно змінювати програми як експлуатування можливо, якщо довільні біти купи виконуються.
BenPen

14

TL; DR

  • Стек виклику як механізм виклику функції:
    1. Зазвичай моделюється обладнанням, але не має принципового значення для побудови апаратних засобів
    2. Є основним для імперативного програмування
    3. Не є принциповим для функціонального програмування
  • Стек як абстракція "останнього, першого виходу" (LIFO) є основоположним для інформатики, алгоритмів і навіть деяких нетехнічних областей.
  • Деякі приклади організації програми, які не використовують стеки викликів:
    • Стиль продовження руху (CPS)
    • Державна машина - гігантська петля, з усім накресленою. (Створюється натхнення архітектурою прошивки Saab Gripen і приписується комунікації Генрі Спенсера та відтворено Джоном Кармаком.) (Примітка №1)
    • Архітектура потоку даних - мережа акторів, з'єднаних чергами (FIFO). Черги іноді називають каналами.

Решта цієї відповіді - випадковий збір думок і анекдотів, а отже дещо неорганізований.


Стек, який ви описали (як механізм виклику функцій), характерний для імперативного програмування.

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

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

  1. Комбінаційна логіка, тобто з'єднання логічних воріт (і, або, ні, ...) Зауважте, що "комбінаційна логіка" виключає зворотні відгуки.
  2. Пам'ять, тобто шльопанці, засувки, регістри, SRAM, DRAM тощо.
  3. Державна машина, яка складається з деякої комбінаційної логіки та деякої пам’яті, достатньо, щоб вона могла реалізувати «контролер», який управляє рештою обладнання.

Наступні дискусії містили велику кількість прикладів альтернативних способів структурування імперативних програм.

Структура такої програми буде виглядати приблизно так:

void main(void)
{
    do
    {
        // validate inputs for task 1
        // execute task 1, inlined, 
        // must complete in a deterministically short amount of time
        // and limited to a statically allocated amount of memory
        // ...
        // validate inputs for task 2
        // execute task 2, inlined
        // ...
        // validate inputs for task N
        // execute task N, inlined
    }
    while (true);
    // if this line is reached, tell the programmers to prepare
    // themselves to appear before an accident investigation board.
    return 0; 
}

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



@ Петерис: Стеки - це структури даних LIFO.
Крістофер Кройцзіг

1
Цікаво. Я б подумав це навпаки. Наприклад, FORTRAN є обов'язковою мовою програмування, а ранні версії не використовували стек викликів. Однак рекурсія є основоположною для функціонального програмування, і я не думаю, що це можливо реалізувати в загальному випадку без використання стека.
ТЕД,

@TED ​​- у реалізації функціональної мови там є стек (або, як правило, дерево) структура даних, що представляє очікувані обчислення, але не обов'язково виконувати її інструкціями, використовуючи режими адресації, орієнтовані на стек, або навіть інструкції виклику / повернення. (вкладеним / рекурсивним способом - можливо, просто частиною циклу станкової машини).
davidbak

@davidbak - IIRC, рекурсивний алгоритм в значній мірі повинен бути рекурсивним, щоб мати можливість позбутися стека. Напевно, є й інші випадки, коли ви могли б оптимізувати це, але в загальному випадку вам доведеться мати стек . Насправді мені сказали, що є математичний доказ цього, що десь плаває. Ця відповідь стверджує, що це теорема Церкви Тьюрінга (я думаю, виходячи з того, що машини Тьюрінга використовують стек?)
ТЕД

1
@TED ​​- Я з вами згоден. Я вважаю, що помилка тут полягає в тому, що я читаю пост ОП, щоб говорити про системну архітектуру, яка була для мене машинною архітектурою . Я думаю, що інші, хто відповів тут, мають таке ж розуміння. Тож ті з нас, хто зрозумів, що це контекст, відповіли, що вам не потрібен стек на рівні машинних інструкцій / адресних режимів. Але я можу бачити, що питання також може бути інтерпретоване так, що означає, чи потрібна мовна система взагалі стек викликів. Ця відповідь також ні, але з різних причин.
davidbak

11

Ні, не обов’язково.

Прочитати стару паперову колекцію Appel Збір сміття може бути швидше, ніж виділення стека . Він використовує стиль проходження продовження і демонструє безладну реалізацію.

Зауважте також, що в старих архітектурах комп'ютерів (наприклад, IBM / 360 ) не було реєстру апаратних стеків. Але ОС і компілятор зарезервували реєстр покажчика стека за умовами (пов'язаний з умовами викликів ), щоб вони могли мати стек програмного виклику .

В принципі, цілий компілятор програми та оптимізатор програми C могли виявити випадок (дещо поширений для вбудованих систем), коли графік викликів є статистично відомим і без рекурсії (або функціональних покажчиків). У такій системі кожна функція могла зберігати свою зворотну адресу у фіксованому статичному розташуванні (саме так Fortran77 працював у комп’ютерах епохи 1970 року).

У ці дні процесори також мають стеки викликів (і інструкції з виклику та повернення машини), відомі про кеші процесора .


1
Досить впевнений, що FORTRAN перестав використовувати статичні місця повернення, коли FORTRAN-66 вийшов та потребував підтримки для SUBROUTINEта FUNCTION. Ти правда для більш ранніх версій (FORTRAN-IV і, можливо, WATFIV).
TMN

І КОБОЛ, звичайно. І чудовий момент щодо IBM / 360 - він отримав досить багато користі, навіть незважаючи на режими адресації апаратних стеків. (R14, я вважаю, що це було?) І в ньому були компілятори для мов на основі стека, наприклад, PL / I, Ada, Algol, C.
davidbak

Дійсно, я вивчив 360 в коледжі і спочатку виявив, що це дивовижно.
JDługosz

1
@ JDługosz Найкращий спосіб сучасним студентам комп'ютерної архітектури розглянути 360 - це дуже проста машина RISC ... хоч і з більш ніж одним форматом інструкцій ... та кількома аномаліями як TRі TRT.
davidbak

Як щодо "нуля та додавання упакованого" для переміщення регістра? Але "гілка та посилання", а не стек для зворотної адреси, зробили повернення.
JDługosz

10

Ви отримали кілька хороших відповідей поки що; дозвольте навести вам непрактичний, але високоосвічений приклад того, як ви могли б спроектувати мову без поняття стеки чи «контрольного потоку». Ось програма, яка визначає фактичні факти:

function f(i) => if i == 0 then 1 else i * f(i - 1)
let x = f(3)

Ми ставимо цю програму в рядок і оцінюємо програму за текстовою підстановою. Отже, коли ми проводимо оцінку f(3), ми робимо пошук і замінюємо 3 на i, наприклад:

function f(i) => if i == 0 then 1 else i * f(i - 1)
let x = if 3 == 0 then 1 else 3 * f(3 - 1)

Чудово. Тепер ми виконуємо ще одну підстановку тексту: ми бачимо, що умова "якщо" - хибне, і замінюємо інший рядок, виробляючи програму:

function f(i) => if i == 0 then 1 else i * f(i - 1)
let x = 3 * f(3 - 1)

Тепер ми робимо ще одну заміну рядків для всіх підвиразів, що включають константи:

function f(i) => if i == 0 then 1 else i * f(i - 1)
let x = 3 * f(2)

І ви бачите, як це йде; Я не буду більше працювати. Ми могли б продовжувати робити ряд підстановок, поки не дійшли до let x = 6нас, і ми не закінчилися

Ми використовуємо стек традиційно для локальних змінних та інформації про продовження; Пам'ятайте, стек не говорить вам, звідки ви прийшли, він говорить вам, куди ви йдете далі, з цією поверненою вартістю в руці.

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

Звичайно, насправді робити заміну рядків - це, мабуть, не шлях. Але мови програмування, які підтримують «екваціональне міркування» (наприклад, Haskell), логічно використовують цю методику.


3
Retina - це приклад мови програмування на основі Regex, який використовує рядкові операції для обчислення.
Ендрю Пілізер

2
@AndrewPiliser Розроблений та реалізований цим крутим чуваком .
кіт

3

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

Модульність

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

Динамічне оцінювання

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

З огляду на те, що будь-який потік (за визначенням) має лише один поточний покажчик інструкцій, LIFO-стек є доречним для відстеження кожного виклику.

Винятки

Отже, поки модель продовження не підтримує структуру даних явно для стека, все ще є вкладене виклик модулів, який треба десь підтримувати!

Навіть декларативні мови або підтримують історію оцінювання, або навпаки вирівнюють план виконання з міркувань ефективності та підтримують прогрес якось іншим чином.

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

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


1
Ви малюєте себе в кутку, передбачаючи " будь -яку багатопоточну систему". З'єднані машини з кінцевим станом можуть мати декілька потоків у своїй реалізації, але не потребують стеки LIFO. У FSM немає обмежень, що ви повертаєтесь до будь-якого попереднього стану, не кажучи вже про порядок LIFO. Отже, це справжня багатопотокова система, для якої вона не відповідає. І якщо ви обмежитеся визначенням багатопотокових даних як "паралельні стеки викликів незалежних функцій", ви отримаєте кільцеве визначення.
MSalters

Я не читаю питання таким чином. OP знайомий з викликами функцій, але запитує про інші системи.
MSalters

@MSalters Оновлено, щоб включити одночасні нескінченні петлі. Модель дійсна, але обмежує масштабованість. Я б припустив, що навіть середньостатистичні машини включають виклики функцій, щоб дозволити повторне використання коду.
Пекка

2

Усі старі мейнфрейми (IBM System / 360) взагалі не мали поняття про стек. Наприклад, на 260, параметри були побудовані у фіксованому місці в пам'яті, і коли викликалася підпрограма, вона викликалася з R1вказівкою на блок параметрів і R14містить зворотну адресу. Викликана програма, якби вона хотіла викликати іншу підпрограму, повинна була б зберігатись R14у відомому місці перед тим, як здійснити цей виклик.

Це набагато надійніше, ніж стек, тому що все може зберігатися у фіксованих місцях пам'яті, встановлених під час компіляції, і це може бути на 100% гарантовано, що процеси ніколи не закінчуються. Немає жодного з "Виділіть 1 МБ і схрестіть пальці", що ми маємо робити сьогодні.

Рекурсивні виклики підпрограми були дозволені в PL / I, вказавши ключове слово RECURSIVE. Вони означали, що пам'ять, яка використовується підпрограмою, динамічно, а не виділяється статично. Але рекурсивні дзвінки були настільки ж рідкісними, як і зараз.

Експлуатація без стоків також значно спрощує масивні багатопотокові нитки, саме тому часто робляться спроби зробити сучасні мови безслідними. Наприклад, немає жодної причини, наприклад, чому компілятор C ++ не міг бути модифікований на зворотній частині для використання динамічно розподіленої пам'яті, а не стеків.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.