ОБНОВЛЕНИЕ: мне настолько понравился этот вопрос, что я сделал его темой моего блога 18 ноября 2011 года .Спасибо за отличный вопрос!
Я всегда задавался вопросом: какова цель стека?
Я предполагаю, что вы имеете в виду оценочный стек языка MSIL, а не фактический стек для потока во время выполнения.
Почему происходит перенос из памяти в стек или «загрузка»?С другой стороны, почему происходит перенос из стека в память или «сохранение»?Почему бы просто не поместить их все в память?
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-to-machine-code также легко написать, потому что CIL - простой язык.Мы избавляемся от всех тонкостей C # и VB и еще чего-то и «опускаем» все до простого языка, для которого легко написать джиттер.
Наличие промежуточного языка снижает стоимость создания нового языкакомпилятор резко .Это также значительно снижает стоимость поддержки нового чипа.Вы хотите поддержать новый чип, вы нашли экспертов по этому чипу и попросили их написать джиттер CIL, и все готово;Затем вы поддерживаете все эти языки на своем чипе.
ОК, поэтому мы установили, почему у нас MSIL;потому что наличие промежуточного языка снижает затраты.Почему тогда язык является «стековым автоматом»?
Поскольку стековые машины концептуально очень просты для писателей языковых компиляторов.Стеки - это простой, понятный механизм описания вычислений.Стековые машины также концептуально очень просты для разработчиков JIT-компиляторов.Использование стека - это упрощенная абстракция, и поэтому, опять же, это снижает наши затраты .
Вы спрашиваете: «Зачем вообще нужен стек?»Почему бы просто не делать все прямо из памяти?Хорошо, давайте подумаем об этом.Предположим, что вы хотите сгенерировать CIL-код для:
int x = A() + B() + C() + 10;
Предположим, у нас есть соглашение, которое «add», «call», «store» и т. Д. Всегда убирает их аргументы из стека и помещает их результат (если есть) в стеке.Чтобы сгенерировать 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
...
Вы видите, как это происходит?Наш код получает огромный , потому что мы должны явно выделить все временное хранилище , которое обычно по соглашению просто помещается в стек .Хуже того, сами наши коды операций становятся огромными, потому что все они теперь должны принимать в качестве аргумента адрес, в который они собираются записать свой результат, и адрес каждого операнда.Инструкция «add», которая знает, что собирается взять две вещи из стека и поместить одну вещь, может быть одним байтом.Инструкция добавления, которая принимает два адреса операнда и адрес результата, будет огромной.
Мы используем основанные на стеке коды операций, потому что стеки решают общую проблему .А именно: Я хочу выделить какое-то временное хранилище, использовать его очень скоро, а затем быстро избавиться от него, когда я закончу .Предполагая, что в нашем распоряжении есть стек, мы можем сделать коды операций очень маленькими, а код - очень кратким.
ОБНОВЛЕНИЕ: Некоторые дополнительные мысли
Кстати, эта идея радикально снизить затраты(1) указание виртуальной машины, (2) написание компиляторов, предназначенных для языка ВМ, и (3) написание реализаций ВМ на различных аппаратных средствах, вообще не является новой идеей.Он не возник с MSIL, LLVM, байт-кодом Java или какой-либо другой современной инфраструктурой.Самая ранняя реализация этой стратегии, о которой я знаю, это машина pcode с 1966 года.
Впервые я лично услышал об этой концепции, когда узнал, как разработчикам Infocom удалось получить Zork отлично работает на разных машинах.Они указали виртуальную машину под названием Z-machine , а затем создали эмуляторы Z-машины для всего оборудования, на котором они хотели запускать свои игры.Это дало огромное преимущество в том, что они могли реализовать управление виртуальной памятью в примитивных 8-битных системах;игра может быть больше, чем умещается в памяти, потому что они могут просто выгружать код с диска, когда им это нужно, и отбрасывать его, когда им нужно загрузить новый код.