ОБНОВЛЕНИЕ: мне очень понравился этот вопрос, поэтому я сделал его темой моего блога 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-машина, а затем создали эмуляторы Z-машины для всего оборудования, на котором они хотели запускать свои игры. Это имело огромное преимущество в том, что они могли реализовывать управление виртуальной памятью в примитивных 8-битных системах; игра может быть больше, чем умещается в памяти, потому что они могут просто выгружать код с диска, когда им это нужно, и отбрасывать его, когда им нужно загрузить новый код.