Ответ 1
ОБНОВЛЕНИЕ: мне так понравился этот вопрос, я сделал его тему моего блога 18 ноября 2011 года. Спасибо за отличный вопрос!
Я всегда задавался вопросом: какова цель стека?
Я предполагаю, что вы имеете в виду стек оценки языка MSIL, а не фактический стек на потоке во время выполнения.
Почему происходит переход из памяти в стек или "загрузка?" С другой стороны, почему происходит переход из стека в память или "хранение"? Почему бы просто не разместить их все в памяти?
MSIL - это язык "виртуальной машины". Компиляторы, такие как компилятор С#, генерируют CIL, а затем во время выполнения другой компилятор, называемый JIT (Just In Time), компилятор превращает IL в фактический машинный код, который может выполняться.
Итак, сначала дайте ответ на вопрос "зачем вообще MSIL?" Почему не просто компилятор С# выписывает машинный код?
Потому что дешевле это сделать. Предположим, мы этого не сделали; предположим, что каждый язык должен иметь собственный генератор машинного кода. У вас есть двадцать разных языков: С#, JScript.NET, Visual Basic, IronPython, F #... И предположим, что у вас есть десять разных процессоров. Сколько генераторов кода вы должны писать? 20 x 10 = 200 генераторов кода. Это много работы. Теперь предположим, что вы хотите добавить новый процессор. Вы должны написать генератор кода для него двадцать раз, по одному для каждого языка.
Кроме того, это трудная и опасная работа. Написание эффективных генераторов кода для чипов, которыми вы не являетесь экспертом, - тяжелая работа! Разработчики компиляторов являются экспертами по семантическому анализу своего языка, а не по эффективному распределению регистров новых наборов микросхем.
Теперь предположим, что мы делаем это способом CIL. Сколько генераторов CIL вам нужно писать? Один на каждый язык. Сколько компиляторов JIT вам нужно писать? Один на процессор. Всего: 20 + 10 = 30 генераторов кода. Более того, генератор языка для CIL легко писать, потому что CIL - простой язык, а генератор кода CIL-to-machine также легко писать, потому что CIL - простой язык. Мы избавляемся от всех тонкостей С# и VB, а то, что еще и "опускаем" все на простой язык, на котором легко написать джиттер для.
Наличие промежуточного языка значительно снижает стоимость создания нового компилятора языка. Это также значительно снижает стоимость поддержки нового чипа. Вы хотите поддержать новый чип, вы найдете некоторых экспертов на этом чипе и попросите их написать дрожание CIL, и все готово; вы затем поддерживаете все эти языки на своем чипе.
ОК, поэтому мы установили, почему у нас MSIL; потому что наличие промежуточного языка снижает затраты. Почему же тогда язык является "стековой машиной"?
Поскольку стековые машины концептуально очень просты для разработчиков компилятора языка. Стеки - это простой, понятный механизм описания вычислений. Статические машины также концептуально очень легки для писателей компилятора JIT. Использование стека является упрощающей абстракцией, и, следовательно, оно снижает наши затраты.
Вы спрашиваете: "Почему у вас вообще есть стек?" Почему бы просто не сделать все прямо из памяти? Ну, подумайте об этом. Предположим, вы хотите сгенерировать код CIL для:
int x = A() + B() + C() + 10;
Предположим, что мы имеем соглашение, что "add", "call", "store" и т.д. всегда вынимают свои аргументы из стека и помещают их результат (если он есть) в стек. Чтобы создать код CIL для этого С#, мы просто скажем что-то вроде:
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) записи реализаций виртуальной машины на различных аппаратных средствах, не является новой идея вообще. Это не связано с MSIL, LLVM, байт-кодом Java или любой другой современной инфраструктурой. Самая ранняя реализация этой стратегии, о которой я знаю, - это машина pcode от 1966 года.
Первое, что я лично слышал об этой концепции, было тогда, когда я узнал, как разработчикам Infocom удалось получить Zork, работающих на стольких разных машинах, так хорошо. Они указали виртуальную машину под названием Z-machine, а затем создали эмуляторы Z-машин для всего оборудования, на котором они хотели запускать свои игры. Это добавило огромную выгоду, что они могли реализовать управление виртуальной памятью на примитивных 8-битных системах; игра может быть больше, чем будет вписываться в память, потому что они могут просто скопировать код с диска, когда он им нужен, и отбросить его, когда им нужно загрузить новый код.