Яке призначення стека? Навіщо нам це потрібно?


320

Тому я зараз навчаюсь MSIL, щоб навчитися налагоджувати свої програми C # .NET.

Я завжди замислювався: яке призначення стека?

Просто щоб поставити моє запитання в контекст:
Чому існує перехід з пам'яті в стек або "завантаження?" З іншого боку, чому відбувається передача з стека в пам'ять або "зберігання"? Чому б просто не помістити їх у пам'ять?

  • Це тому, що це швидше?
  • Це тому, що на основі оперативної пам'яті?
  • Для ефективності?

Я намагаюся зрозуміти це, щоб допомогти мені зрозуміти коди CIL набагато глибше.


28
Стек - це одна частина пам’яті, подібно до купи - інша частина пам’яті.
CodesInChaos

@CodeInChaos Ви говорите про типи значень та типи посилань? чи це те саме з точки зору IL-кодів? ... Я знаю, що стек просто швидший і ефективніший, ніж купа (але це у світі
цін

15
@CodeInChaos - Я думаю, що стек, на який посилається Ян, - це стекова машина, проти якої написаний IL, на відміну від області пам'яті, яка приймає кадри стека під час викликів функцій. Вони два різні стеки, і після JIT, стека IL не існує (на x86, все одно)
Damien_The_Unbeliever

4
Яким чином знання MSIL допоможуть налагодити додатки .NET?
Пьотр Перак

1
На сучасних машинах кешування поведінки коду - це виробник і вимикач продуктивності. Пам'ять є скрізь. Стек, як правило, просто тут. Якщо припустити, що стек - це реальна річ, а не просто поняття, яке використовується для вираження дії деякого коду. При впровадженні платформи MSIL, що працює на платформі MSIL, немає потреби в тому, щоб концепція стека перетворювалась на апаратне забезпечення, яке фактично штовхає біти навколо.
Відновіть Моніку

Відповіді:


441

ОНОВЛЕННЯ: Це питання мені так сподобалось, що я став його темою свого блогу 18 листопада 2011 року . Дякую за чудове запитання!

Я завжди замислювався: яке призначення стека?

Я припускаю, що ви маєте на увазі стек оцінки мови MSIL, а не фактичний стек per-thread під час виконання.

Чому відбувається передача з пам'яті в стек або "завантаження?" З іншого боку, чому відбувається передача з стека в пам'ять або "зберігання"? Чому б просто не помістити їх у пам'ять?

MSIL - мова "віртуальної машини". Компілятори, такі як компілятор C #, генерують CIL , а потім під час виконання інший компілятор під назвою компілятор JIT (Just In Time) перетворює IL в фактичний машинний код, який може виконати.

Тож спочатку давайте відповімо на питання "навіщо взагалі MSIL?" Чому б не просто компілятор C # виписати машинний код?

Тому що дешевше це зробити так. Припустимо, ми не зробили це так; припустимо, кожна мова повинна мати власний генератор машинного коду. У вас двадцять різних мов: C #, JScript .NET , Visual Basic, IronPython , F # ... І припустимо, у вас є десять різних процесорів. Скільки генераторів коду потрібно написати? 20 х 10 = 200 генераторів коду. Це багато роботи. Тепер припустимо, ви хочете додати новий процесор. Ви повинні написати генератор коду для нього двадцять разів, по одному для кожної мови.

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

Тепер припустимо, що ми робимо це CIL способом. Скільки генераторів CIL потрібно написати? Один на мову. Скільки компіляторів JIT потрібно написати? Один на процесор. Всього: 20 + 10 = 30 генераторів коду. Більше того, генератор мови на CIL легко писати, оскільки CIL - це проста мова, а генератор коду CIL до машини також легко записати, оскільки CIL - це проста мова. Ми позбавляємося від усіх тонкощів C # і VB, а ще чого, і «опускаємо» все до простої мови, на яку легко написати джитер.

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

Гаразд, тому ми встановили, чому у нас MSIL; тому що наявність проміжної мови знижує витрати. Чому тоді мова є "машиною стека"?

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

Ви запитуєте "навіщо взагалі мати стек?" Чому б просто не зробити все безпосередньо з пам'яті? Що ж, давайте подумаємо над цим. Припустимо, ви хочете генерувати код CIL для:

int x = A() + B() + C() + 10;

Припустимо, у нас є умова, що "додавати", "дзвонити", "зберігати" і так далі завжди знімають свої аргументи зі стека і ставлять їх результат (якщо такий є) на стек. Щоб створити код CIL для цього C #, ми просто скажемо щось на зразок:

load the address of x // The stack now contains address of x
call A()              // The stack contains address of x and result of A()
call B()              // Address of x, result of A(), result of B()
add                   // Address of x, result of A() + B()
call C()              // Address of x, result of A() + B(), result of C()
add                   // Address of x, result of A() + B() + C()
load 10               // Address of x, result of A() + B() + C(), 10
add                   // Address of x, result of A() + B() + C() + 10
store in address      // The result is now stored in x, and the stack is empty.

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

Allocate temporary store T1 for result of A()
Call A() with the address of T1
Allocate temporary store T2 for result of B()
Call B() with the address of T2
Allocate temporary store T3 for the result of the first addition
Add contents of T1 to T2, then store the result into the address of T3
Allocate temporary store T4 for the result of C()
Call C() with the address of T4
Allocate temporary store T5 for result of the second addition
...

Бачите, як це йде? Наш код стає величезним, тому що ми повинні чітко виділити всі тимчасові сховища, які, як правило, за умовою просто переходять на стек . Гірше, що самі наші опкоди стають величезними, тому що всі вони тепер повинні брати за аргумент адресу, в яку вони збираються записати свій результат, та адресу кожного операнда. Інструкція "додати", яка знає, що збирається зняти дві речі зі стека і поставити одну річ, може бути одним байтом. Додана інструкція, що займає дві адреси операндів і адреса результату, буде величезною.

Ми використовуємо опкоди на основі стека, оскільки стеки вирішують загальну проблему . А саме: я хочу виділити деяке тимчасове сховище, використовувати його дуже скоро, а потім швидко позбутися від нього, коли я закінчу . Зробивши припущення, що у нас є стек у нашому розпорядженні, ми можемо зробити опкоди дуже маленькими, а код дуже лаконічним.

ОНОВЛЕННЯ: Деякі додаткові думки

Між іншим, ця ідея різкого зниження витрат за допомогою (1) визначення віртуальної машини, (2) написання компіляторів, орієнтованих на мову VM, та (3) написання реалізацій VM на різних апаратних засобах, зовсім не нова ідея. . Він не походить із MSIL, LLVM, байт-кодом Java або будь-якою іншою сучасною інфраструктурою. Найбільш рання реалізація цієї стратегії, яку я знаю, - це машина з кодами з 1966 року.

Перше, що я особисто чув про цю концепцію, було, коли я дізнався, як реалізатори Infocom спромоглися так, щоб Zork так добре працював на багатьох різних машинах. Вони вказали віртуальну машину під назвою Z-machine, а потім зробили емулятори Z-machine для всього обладнання, на якому вони хотіли запустити свої ігри. Це дало величезну перевагу, що вони могли реалізувати управління віртуальною пам'яттю в примітивних 8-бітових системах; гра може бути більшою, ніж помістилася б у пам'яті, оскільки вони могли просто перезавантажити код з диска, коли їм це потрібно, і відкинути його, коли потрібно завантажити новий код.


63
ОГО. Це просто ТОЧНО те, що я шукав. Найкращий спосіб отримати відповідь - це отримати її від самого головного розробника. Дякую за витрачений час, і я впевнений, що це допоможе всім, хто дивується тонкощам компілятора та MSIL. Дякую Еріку.
Ян Карло Вірай

18
Це була чудова відповідь. Нагадує, чому я читаю ваш блог, хоча я хлопець на Java. ;-)
jprete

34
@JanCarloViray: Вас дуже вітають! Хочу зазначити , що я головний розробник, що не головний розробник. У цій команді є кілька людей з такою назвою, і я навіть не найстарший з них.
Ерік Ліпперт

17
@Eric: Якщо / коли ви коли-небудь перестанете любити кодування, вам варто подумати про навчання програмістів. Крім веселощів, ви можете робити вбивство без тиску бізнесу. Дивовижний нюх - це те, що ви отримали в цій галузі (і чудове терпіння, можу додати). Я це кажу, як екс-університетський викладач.
Алан

19
Близько 4 абзаців я говорив собі: "Це звучить як Ерік", до 5-го чи 6-го я закінчив "Так, безумовно, Ерік" :) Ще одна справді & епічно вичерпна відповідь.
Бінарний страшник

86

Майте на увазі, що коли ви говорите про MSIL, то ви говорите про інструкції для віртуальної машини. Віртуальний комп'ютер, що використовується в .NET, - це віртуальна машина на основі стека. На відміну від VM на базі реєстру, приклад цього , Dalvik VM, який використовується в операційних системах Android, є.

Стек у віртуальному віртуальному комп'ютері віртуальний, перекладач інструкцій VM в фактичний код, який працює на процесорі, залежить від інтерпретатора чи просто вчасно встановленого компілятора. Що стосується .NET майже завжди тремтіння, набір інструкцій MSIL був розроблений таким чином, щоб його не було. Наприклад, на відміну від байт-коду Java, він має чіткі вказівки щодо операцій з певними типами даних. Що робить його оптимізованим для інтерпретації. Інтерпретатор MSIL насправді існує, хоча він використовується в .NET Micro Framework. Який працює на процесорах з дуже обмеженими ресурсами, не може дозволити собі оперативну пам'ять, необхідну для зберігання машинного коду.

Фактична модель машинного коду є змішаною, має як стек, так і регістри. Однією з великих завдань оптимізатора коду JIT є розробка способів зберігання змінних, які зберігаються в стеку, в регістрах, що значно підвищує швидкість виконання. Противоположну проблему має тремтіння Дальвіка.

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


2
+1 за дуже детальне пояснення, і +100 (якщо я міг) для додаткового ДЕТАЛЬНОГО порівняння з іншими системами та мовою :)
Ян Карло Вірай

4
Чому Dalvik є машиною реєстрації? Sicne в основному орієнтована на ARM-процесори. Тепер x86 має однакову кількість реєстрів, але, будучи CISC, лише 4 з них справді корисні для зберігання місцевих жителів, оскільки решта неявно використовуються в загальних інструкціях. З іншого боку, архітектури ARM мають набагато більше регістрів, які можна використовувати для зберігання місцевих жителів, отже, вони спрощують модель виконання на основі реєстру.
Йоганнес Рудольф

1
@JohannesRudolph Це не так уже майже два десятиліття. Тільки тому, що більшість компіляторів C ++ все ще націлені на набір інструкцій x86 90-х років, не означає, що x86 сам по собі є недостатньою. Haswell має 168 регістрів загального призначення і цілих 168 регістрів AVX, наприклад - набагато більше, ніж будь-який процесор ARM, про який я знаю. Ви можете користуватися всіма з монтажу (сучасного) x86 будь-яким способом. Звинувачуйте письменників-компіляторів, а не архітектуру / процесор. Насправді, це одна з причин, чому проміжна компіляція настільки приваблива - один бінарний, найкращий код для даного процесора; не маніпулюючи архітектурою віків 90-х років.
Луань

2
@JohannesRudolph Компілятор .NET JIT насправді дуже використовує регістри; стек - це переважно абстракція віртуальної машини IL, код, який насправді працює на вашому процесорі, дуже різний. Виклики методів можуть бути регістром пропуску, місцеві жителі можуть бути зняті до регістрів ... Основна перевага стека в машинному коді - це ізоляція, яку він надає підпрограмним викликам - якщо ви введете локальний в регістр, виклик функції може зробити ти втрачаєш цю цінність, і ти насправді не можеш сказати.
Луань

1
@RahulAgarwal Створений машинний код може або не може використовувати стек для будь-якого заданого локального або проміжного значення. В IL, кожен аргумент і локальний знаходиться в стеку - але в машинному коді, це НЕ так (це дозволено, але не обов'язково). Деякі речі корисні в стеку, і вони ставляться на стеку. Деякі речі корисні на купі, і їх кладуть на купу. Деякі речі взагалі не потрібні або потрібно лише кілька моментів у реєстрі. Дзвінки можуть бути виключені повністю (впорядковані) або їх аргументи можуть бути передані в регістри. JIT має багато свободи.
Луань

20

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

  • Дуже компактний об'єктний код
  • Прості укладачі / прості перекладачі
  • Мінімальний стан процесора

-1 @xanatos Чи можете ви спробувати узагальнити заголовки, які ви взяли?
Тім Ллойд

@chibacity Якби я хотів їх узагальнити, я би зробив відповідь. Я намагався врятувати дуже гарне посилання.
xanatos

@xanatos Я розумію ваші цілі, але поділитися посиланням на таку велику статтю у Вікіпедії - не чудова відповідь. Це не важко знайти, просто гуглившись. З іншого боку, Ганс має приємну відповідь.
Тім Ллойд

@chibacity ОП, мабуть, ліниво не спочатку шукати. Тут відповідав хороший посилання (не описуючи його). Два зла приносять одне добро :-) І я підтримаю Ганса.
xanatos

до Answerer та @xanatos +1 для ВЕЛИКОГО посилання. Я чекав, коли хтось повністю узагальнить і отримає відповідь на пакет знань .. якби Ганс не дав відповіді, я зробив би вашу як прийняту відповідь. Це просто було просто посиланням, так що не ярмарок для Ганса, який добре поставив свою відповідь .. :)
Ян Карло Вірай

8

Щоб додати трохи більше запитання про стек. Концепція стека походить від дизайну процесора, коли машинний код в арифметичній логічній одиниці (ALU) працює на операндах, які знаходяться в стеку. Наприклад, операція множення може взяти два верхні операнди зі стека, помножити їх і повернути результат назад у стек. Машина машини зазвичай має дві основні функції додавання та видалення операндів із стека; PUSH і POP. У багатьох dsp-процесорах (цифровий процесор сигналу) і контролерах машин (наприклад, у керуванні пральною машиною) стек розташований на самому чіпі. Це забезпечує швидший доступ до ALU та консолідує необхідну функціональність в єдиний чіп.


5

Якщо концепція стека / купи не дотримується і дані завантажуються в місце випадкової пам'яті АБО дані зберігаються з випадкової пам'яті ... це буде дуже неструктуровано та некеровано.

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


4

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

Дивіться старі праці Ендрю Апеля: Збірник із продовженнями та збиранням сміття може бути швидше, ніж виділення стека

(Він може бути трохи помиляється сьогодні через проблеми з кешем)


0

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

Існує також сім'я мов на основі стека, які називаються конкатенативними мовами . Всі вони (я вважаю) функціональними мовами, тому що стек є неявним переданим параметром, а також змінений стек - це неявне повернення від кожної функції. І Forth, і Factor (що є чудовим) - це приклади разом з іншими. Фактор використовувався аналогічно Луа для ігор сценаріїв, і його написав Слава Пестов, геній, який зараз працює в Apple. Його Google TechTalk на youtube я переглядав кілька разів. Він говорить про конструкторів Боа, але я не впевнений, що він має на увазі ;-).

Я дійсно думаю, що деякі з нинішніх машин, таких як JVM, Microsoft CIL, і навіть той, який я бачив, був написаний для Lua, повинні бути написані деякими з цих мов на основі стека, щоб зробити їх портативними для ще більшої кількості платформ. Я думаю, що ці мови конкатенації якимось чином не вистачають на їх виклики як набори створення віртуальних машин, так і платформи переносимості. Існує навіть pForth, "портативний" Forth, написаний на ANSI C, який можна використовувати для ще більш універсальної портативності. Хтось намагався скомпілювати його за допомогою Emscripten або WebAssembly?

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

У Easy Forth , приємному онлайн-підручнику, написаному на JavaScript, ось невеликий зразок (зверніть увагу на "sq sq sq" як приклад стилю виклику з нульовою точкою):

: sq dup * ;  ok
2 sq . 4  ok
: ^4 sq sq ;  ok
2 ^4 . 16  ok
: ^8 sq sq sq sq ;  ok
2 ^8 . 65536  ok

Крім того, якщо ви подивитесь на джерело веб-сторінок Easy Forth, ви побачите внизу, що це дуже модульно, написане приблизно в 8 файлах JavaScript.

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

Для чого нам потрібен стек - ефективність, оптимізація, нульова точка, збереження регістрів при перериванні, а для рекурсивних алгоритмів - це "правильна форма".

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