Какова цель стека? Зачем нам это нужно?

Итак, сейчас я изучаю MSIL, чтобы научиться отлаживать свои приложения на С#.NET.

Я всегда задавался вопросом: какова цель стека?

Просто чтобы задать свой вопрос в контексте:
Почему происходит переход из памяти в стек или "загрузка?" С другой стороны, почему происходит переход из стека в память или "хранение"? Почему бы просто не разместить их все в памяти?

  • Это потому, что это быстрее?
  • Это потому, что на нем основана операционная система?
  • Для эффективности?

Я пытаюсь понять это, чтобы помочь мне понять код CIL гораздо глубже.

Ответ 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-битных системах; игра может быть больше, чем будет вписываться в память, потому что они могут просто скопировать код с диска, когда он им нужен, и отбросить его, когда им нужно загрузить новый код.

Ответ 2

Имейте в виду, что когда вы говорите о MSIL, вы говорите о инструкциях для виртуальной машины. VM, используемая в .NET, представляет собой виртуальную машину на основе стека. В отличие от VM, основанной на регистре, примером этого является Dalvik VM, используемая в операционных системах Android.

Стек в виртуальной машине виртуальный, для интерпретатора команд VM в настоящий код, который выполняется на процессоре, зависит от интерпретатора или компилятора "точно в момент времени". Что в случае .NET почти всегда является джиттером, набор инструкций MSIL был разработан для того, чтобы быть включенным в игру. В отличие от Java-байт-кода, например, он имеет различные инструкции для операций с конкретными типами данных. Это делает его оптимизированным для интерпретации. Интерпретатор MSIL фактически существует, хотя он используется в .NET Micro Framework. Что работает на процессорах с очень ограниченными ресурсами, не может позволить RAM, необходимые для хранения машинного кода.

Модель машинного кода смешанна, имеет как стек, так и регистры. Одна из больших задач оптимизатора кода JIT заключается в том, чтобы придумать способы хранения переменных, которые хранятся в стеке в регистрах, что значительно улучшает скорость выполнения. Тревога Дальвика имеет противоположную проблему.

Стойка машины в остальном - это очень базовое хранилище, которое очень долгое время было в дизайне процессоров. Он имеет очень хорошую локальность ссылок, очень важную особенность современных процессоров, которые перехватывают данные намного быстрее, чем оперативная память может предоставить его и поддерживает рекурсию. На языковой дизайн в значительной степени влияет наличие стека, видимого в поддержку локальных переменных и ограничений, ограниченных телом метода. Существенной проблемой для стека является тот, для которого назван этот сайт.

Ответ 3

Есть очень интересная/подробная статья Википедии об этом, Преимущества наборов инструкций стека машин. Мне нужно будет процитировать его полностью, поэтому проще просто поместить ссылку. Я просто процитирую субтитры

  • Очень компактный объектный код
  • Простые компиляторы/простые интерпретаторы
  • Минимальное состояние процессора

Ответ 4

Чтобы добавить немного к вопросу о стеке. Концепция стека основана на проектировании ЦП, где машинный код в арифметическом логическом блоке (ALU) работает с операндами, расположенными в стеке. Например, операция умножения может принимать два верхних операнда из стека, несколько их и возвращать результат в стек. Язык машины обычно имеет две основные функции для добавления и удаления операндов из стека; PUSH и POP. Во многих CPU (цифровой процессор сигналов) и контроллерах (например, управляющих стиральной машиной) стек находится на самой микросхеме. Это дает более быстрый доступ к ALU и объединяет требуемую функциональность в один чип.

Ответ 5

Если концепция стека/кучи не соблюдается, и данные загружаются в произвольное место памяти ИЛИ данные хранятся в случайных ячейках памяти... он будет очень неструктурированным и неуправляемым.

Эти понятия используются для хранения данных в предопределенной структуре для повышения производительности, использования памяти... и, следовательно, называемых структур данных.

Ответ 6

Можно работать с системой без стеков, используя стиль продолжения прохождения кодирования. Тогда кадры вызова становятся продолжениями, выделенными в собранной кучей мусора (сборщику мусора потребуется некоторый стек).

См. старые статьи А. Эппеля: Компиляция с продолжением и Сбор мусора может быстрее, чем Stack Allocation

(Он может быть немного не прав сегодня из-за проблем с кешем)