ОБНОВЛЕНИЕ: мне так понравился этот вопрос, что я сделал его темой своего блога 18 ноября 2011. Спасибо за отличный вопрос!
Мне всегда было интересно: для чего нужен стек?
Я предполагаю, что вы имеете в виду стек оценки языка MSIL, а не фактический стек для каждого потока во время выполнения.
Почему происходит передача из памяти в стек или "загрузка"? С другой стороны, почему происходит передача из стека в память или «запоминание»? Почему бы просто не поместить их все в память?
MSIL - это язык «виртуальных машин». Компиляторы, такие как компилятор C #, генерируют CIL, а затем во время выполнения другой компилятор, называемый JIT (Just In Time ) компилятор превращает IL в реальный машинный код, который может выполняться.
Итак, сначала давайте ответим на вопрос "зачем вообще нужен MSIL?" Почему бы просто компилятору C # не записать машинный код?
Потому что так сделать дешевле. Предположим, мы этого не сделали; предположим, что у каждого языка должен быть свой собственный генератор машинного кода. У вас двадцать разных языков: C #, JScript .NET, Visual Basic, IronPython, F # ... Предположим, у вас десять разных процессоров. Сколько генераторов кода вам нужно написать? 20 x 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) написания компиляторов, ориентированных на язык виртуальной машины, и (3) написания реализаций виртуальной машины на различном оборудовании, вовсе не нова. . Он не возник на основе MSIL, LLVM, байт-кода Java или каких-либо других современных инфраструктур. Самая ранняя реализация этой стратегии, о которой я знаю, - это pcode machine 1966 года.
Впервые я лично услышал об этой концепции, когда узнал, как разработчикам Infocom удалось запустить Zork на стольких разных машинах и так хорошо. Они указали виртуальную машину под названием Z-machine, а затем создали эмуляторы Z-машины для всех оборудование, на котором они хотели запускать свои игры. Дополнительным огромным преимуществом было то, что они могли реализовать управление виртуальной памятью на примитивных 8-битных системах; игра могла быть больше, чем поместилась бы в память, потому что они могли просто выгружать код с диска, когда он им нужен, и отбрасывать его, когда им нужно было загрузить новый код.
person
Eric Lippert
schedule
24.10.2011