ОНОВЛЕННЯ: Це питання мені так сподобалось, що я став його темою свого блогу 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-бітових системах; гра може бути більшою, ніж помістилася б у пам'яті, оскільки вони могли просто перезавантажити код з диска, коли їм це потрібно, і відкинути його, коли потрібно завантажити новий код.