Для чего нужен стек? Зачем нам это нужно?

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

Я всегда задавался вопросом: для чего нужен стек?

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

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

Я пытаюсь понять это, чтобы лучше понять коды CIL.


person Jan Carlo Viray    schedule 24.10.2011    source источник
comment
Стек - это одна часть памяти, так же как куча - это другая часть памяти.   -  person CodesInChaos    schedule 24.10.2011
comment
@CodeInChaos, вы говорите о типах значений и ссылочных типах? Или это то же самое в отношении кодов IL? ... Я знаю, что стек просто быстрее и эффективнее, чем куча (но это в мире типов значений / ссылок ... которые я не знаю, здесь то же самое?)   -  person Jan Carlo Viray    schedule 24.10.2011
comment
@CodeInChaos - я думаю, что стек, на который ссылается Ян, является стековой машиной, против которой написан IL, в отличие от области памяти, которая принимает кадры стека во время вызовов функций. Это два разных стека, и после JIT стек IL не существует (по крайней мере, на x86)   -  person Damien_The_Unbeliever    schedule 24.10.2011
comment
@Damien_The_Unbeliever - я даже не знал этого. знаете ли вы какие-либо материалы, ссылки, которые описывают то, что вы только что упомянули? Или, может быть, хотите объяснить это немного подробнее? Итак, машина Stack IL написана против физического VS Stack / Heap, что более логично? Спасибо за помощь.   -  person Jan Carlo Viray    schedule 24.10.2011
comment
Как знание MSIL поможет вам отлаживать .NET-приложения?   -  person Piotr Perak    schedule 25.10.2011
comment
На современных машинах поведение кода при кешировании является фактором, влияющим на производительность. Память везде. Стек обычно находится здесь. Предполагая, что стек - это реальная вещь, а не просто концепция, используемая для выражения работы некоторого кода. При реализации платформы, работающей под управлением MSIL, не требуется, чтобы концепция стека соответствовала аппаратному обеспечению, которое фактически подталкивает биты.   -  person Kuba hasn't forgotten Monica    schedule 18.03.2014


Ответы (7)


ОБНОВЛЕНИЕ: мне так понравился этот вопрос, что я сделал его темой своего блога 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
comment
УХ ТЫ. Это ТОЧНО то, что я искал. Лучший способ получить ответ - это получить его у самого главного разработчика. Спасибо за потраченное время, и я уверен, что это поможет всем, кто интересуется тонкостями компилятора и MSIL. Спасибо, Эрик. - person Jan Carlo Viray; 24.10.2011
comment
Это был отличный ответ. Напоминает мне, почему я читаю ваш блог, хотя я парень Java. ;-) - person jprete; 24.10.2011
comment
@JanCarloViray: Добро пожаловать! Я отмечаю, что я главный разработчик, а не главный разработчик. В этой команде есть несколько человек с таким названием, и я даже не самый старший из них. - person Eric Lippert; 24.10.2011
comment
@EricLippert В чем разница между стеком оценки и стеком для каждого потока. Я понимаю, что все локальные переменные и т. Д. Для потока идут в стек. Что еще мне не хватает? - person Sandeep; 25.10.2011
comment
@Sandeep: стек оценки виртуальной машины и системный стек фактического потока не обязательно должны иметь какие-либо отношения. Виртуальная машина и реальная машина логически совершенно разные. Локальные переменные не должны попадать в системный стек. Некоторые локальные переменные помещаются в стек, некоторые - в регистры, некоторые - в кучу, некоторые полностью удаляются. - person Eric Lippert; 25.10.2011
comment
@Eric: Если / когда вы когда-нибудь перестанете любить кодирование, вам стоит подумать о том, чтобы учить программистов. Помимо удовольствия, вы могли бы совершить убийство без давления со стороны бизнеса. Великолепное чутье - вот что у вас есть в этой области (и, я мог бы добавить, замечательное терпение). Я говорю это как бывший преподаватель университета. - person Alan; 25.10.2011
comment
Примерно 4 абзаца в том, что я говорил себе. Это похоже на Эрика, к 5-му или 6-му я перешла на Ага, определенно Эрик :) Еще один поистине и эпично исчерпывающий ответ. - person Binary Worrier; 25.10.2011
comment
Как всегда молодец, Эрик. Хотя я мог бы спросить ... поскольку новые разновидности процессоров, кажется, появляются все чаще и чаще, как команда справляется с JIT-компиляторами? Например, если Bulldozer + Windows 8 должны стать коленями пчелы, будет ли пакет .NET 4.5 тем, чья JIT будет правильно нацеливаться на него? Или будет обновление .NET 4.0, чтобы дать JITter больше целей? Спасибо. - person Jesse C. Slicer; 25.10.2011
comment
@Jesse: Как команда CLR решает приоритезировать реализацию новых джиттеров для новых платформ, которые поддерживает Windows, конечно, полностью зависит от них, чтобы договориться с командой Windows. Как член группы разработчиков компилятора C # я лично не участвую в этих переговорных сессиях или связанных с ними встречах по планированию. - person Eric Lippert; 25.10.2011
comment
@ JesseC.Slicer: Обычно вы не пишете полный JIT-компилятор для нового процессора; вы потенциально могли бы добавить некоторые оптимизации или обходные пути к существующему JIT-компилятору. Компилятор JIT нацелен на архитектуру, а не на конкретный процессор - так, например, прямо сейчас есть компиляторы x86 JIT и x64 JIT. Можно было бы предположить, что для Win8 у них будет компилятор ARM JIT. Каждый из них будет иметь определенные настройки для разных процессоров, но будет полностью (в идеале) работать на любом оборудовании, которое правильно реализует эту архитектуру. - person Ron Warholic; 26.10.2011
comment
@EricLippert Я хотел бы задать один вопрос о IL: есть ли в Visual Studio возможность увидеть сгенерированный промежуточный язык (IL) для написанного кода C # во время выполнения этого кода .... - person Enigma State; 31.12.2011
comment
@pratapk: мне ничего не известно. Вы всегда можете использовать ILDASM, чтобы увидеть, какой IL создается в данной сборке. (И есть плагин, который позволяет вам видеть IL кода, сгенерированного для облегченных делегатов кодогенератора.) - person Eric Lippert; 31.12.2011

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

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

Фактическая модель машинного кода является смешанной и имеет как стек, так и регистры. Одна из основных задач оптимизатора JIT-кода - найти способы хранения переменных, которые хранятся в стеке, в регистрах, что значительно повысит скорость выполнения. У дрожания Dalvik есть противоположная проблема.

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

person Hans Passant    schedule 24.10.2011
comment
+1 за очень подробное объяснение и +100 (если можно) за дополнительное ПОДРОБНОЕ сравнение с другими системами и языком :) - person Jan Carlo Viray; 24.10.2011
comment
Почему Dalvik - это регистрационная машина? Sicne в первую очередь ориентирована на процессоры ARM. Теперь x86 имеет такое же количество регистров, но, будучи CISC, только 4 из них действительно могут использоваться для хранения локальных переменных, потому что остальные неявно используются в общих инструкциях. Архитектура ARM, с другой стороны, имеет намного больше регистров, которые можно использовать для хранения локальных переменных, следовательно, они упрощают модель выполнения на основе регистров. - person Johannes Rudolph; 28.10.2011
comment
Многие варианты выбора в Dalvik могут быть результатом ограничений IP. Google пришлось очень постараться, чтобы убедить судей, что они не нарушают права Oracle JVM, чтобы избежать ограничений лицензирования Java. Выбор представления виртуальной машины в любом случае существенно не влияет на производительность среды выполнения в среде JIT. - person Pekka; 30.01.2013
comment
@JohannesRudolph Этого не было уже почти два десятилетия. Тот факт, что большинство компиляторов C ++ по-прежнему нацелены на набор инструкций x86 90-х годов, не означает, что x86 сам по себе является недостаточным. Например, Haswell имеет 168 целочисленных регистров общего назначения и 168 регистров GP AVX - намного больше, чем у любого известного мне процессора ARM. Вы можете использовать все из (современной) сборки x86 как хотите. Винить в этом составителей компилятора, а не архитектуру / процессор. Фактически, это одна из причин, почему промежуточная компиляция так привлекательна - один двоичный код, лучший для данного процессора; не возиться с архитектурой эпохи 90-х. - person Luaan; 09.12.2016
comment
@JohannesRudolph Компилятор .NET JIT на самом деле довольно интенсивно использует регистры; стек - это в основном абстракция виртуальной машины IL, код, который на самом деле выполняется на вашем ЦП, сильно отличается. Вызов методов может быть переданным регистром, локальные переменные могут быть перенесены в регистры ... Основным преимуществом стека в машинном коде является изоляция, которую он дает вызовам подпрограмм - если вы поместите локальное значение в регистр, вызов функции может сделать вы теряете эту ценность и не можете точно сказать. - person Luaan; 09.12.2016
comment
@Luaan: не могли бы вы подробнее рассказать об этом стек в основном представляет собой абстракцию виртуальной машины IL на самом деле мне немного сложно визуализировать - JIT в основном соответствует IL машинному коду, и этот машинный код - это то, что оценивается в стеке, верно? Тогда где подходит стек потоков .. немного запутался :( цените, если вы можете указать мне какой-либо источник, где эти детали четко объяснены .. - person rahulaga_dev; 07.04.2018
comment
@RahulAgarwal Сгенерированный машинный код может или не может использовать стек для любого данного локального или промежуточного значения. В IL все аргументы и local находятся в стеке, но в машинном коде это не истина (это разрешено, но не обязательно). Некоторые вещи полезны в стеке, и они помещаются в стек. Некоторые вещи полезны в кучу, и они помещаются в кучу. Некоторые вещи вообще не нужны, или их нужно записать всего на несколько минут. Вызовы могут быть полностью исключены (встроены) или их аргументы могут быть переданы в регистры. JIT обладает большой свободой. - person Luaan; 07.04.2018

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

  • Очень компактный объектный код
  • Простые компиляторы / простые интерпретаторы
  • Минимальное состояние процессора
person Community    schedule 24.10.2011
comment
-1 @xanatos Не могли бы вы обобщить взятые вами заголовки? - person Tim Lloyd; 24.10.2011
comment
@chibacity Если бы я хотел их резюмировать, я бы ответил. Я пытался спасти очень хорошую ссылку. - person xanatos; 24.10.2011
comment
@xanatos Я понимаю ваши цели, но поделиться ссылкой на такую ​​большую статью в Википедии - не лучший ответ. Это не сложно найти, просто погуглил. С другой стороны, у Ганса есть хороший ответ. - person Tim Lloyd; 24.10.2011
comment
@chibacity ОП, вероятно, ленился не искать первым. Ответивший тут дал хорошую ссылку (не описывая ее). Два зла делают одно добро :-) И я буду голосовать за Ганса. - person xanatos; 24.10.2011
comment
автору ответа и @xanatos +1 для ОТЛИЧНОЙ ссылки. Я ждал, что кто-то полностью резюмирует и получит ответ в виде пакета знаний ... если бы Ганс не дал ответа, я бы сделал ваш принятым ответом ... просто это была просто ссылка, так что это было несправедливо по отношению к Гансу, который приложил большие усилия для своего ответа .. :) - person Jan Carlo Viray; 24.10.2011

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

person skyman    schedule 26.10.2011

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

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

person Azodious    schedule 24.10.2011

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

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

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

person Basile Starynkevitch    schedule 29.10.2011

Я искал "прерывание", и никто не считал это преимуществом. Для каждого устройства, которое прерывает работу микроконтроллера или другого процессора, обычно есть регистры, которые помещаются в стек, вызывается процедура обслуживания прерывания, и когда это выполняется, регистры извлекаются из стека и возвращаются туда, где они мы. Затем указатель инструкции восстанавливается, и нормальная активность возобновляется с того места, где она была остановлена, как если бы прерывания никогда не было. Со стеком вы можете фактически иметь несколько устройств (теоретически), прерывающих друг друга, и все это просто работает - из-за стека.

Существует также семейство языков на основе стека, называемых конкатенативными языками. Все они (я считаю) функциональные языки, потому что стек - это неявный переданный параметр, а также измененный стек - это неявный возврат от каждой функции. И Forth, и Factor (что превосходно) являются примерами наряду с другими. Factor использовался аналогично Lua для написания сценариев игр и был написан Славой Пестовым, гением, который в настоящее время работает в Apple. Его Google TechTalk на YouTube я смотрел несколько раз. Он говорит о конструкторах Boa, но я не совсем понимаю, что он имеет в виду ;-).

Я действительно думаю, что некоторые из текущих виртуальных машин, такие как JVM, Microsoft CIL и даже та, которую я видел, была написана для Lua, должны быть написаны на некоторых из этих основанных на стеке языков, чтобы сделать их переносимыми на еще большее количество платформ. Я думаю, что этим конкатенативным языкам почему-то не хватает того, что они называли наборами для создания виртуальных машин и платформами переносимости. Существует даже pForth, «переносимый» Forth, написанный на ANSI C, который можно использовать для еще более универсальной переносимости. Кто-нибудь пробовал скомпилировать его с помощью Emscripten или WebAssembly?

Для языков на основе стека существует стиль кода, называемый нулевой точкой, потому что вы можете просто перечислить функции, которые должны быть вызваны, без передачи каких-либо параметров вообще (время от времени). Если бы функции идеально подходили друг к другу, у вас не было бы ничего, кроме списка всех функций с нулевой точкой, и это было бы ваше приложение (теоретически). Если вы углубитесь в Forth или Factor, вы поймете, о чем я говорю.

В Easy Forth, хорошем онлайн-руководстве, написанном на JavaScript, вот небольшой образец (обратите внимание на " sq sq sq sq "как пример стиля вызова с нулевой точкой):

: sq dup * ;  ok
2 sq . 4  ok
: ^4 sq sq ;  ok
2 ^4 . 16  ok
: ^8 sq sq sq sq ;  ok
2 ^8 . 65536  ok

Кроме того, если вы посмотрите на исходный код веб-страницы Easy Forth, вы увидите внизу, что она очень модульная и написана примерно в 8 файлах JavaScript.

Я потратил много денег почти на каждую книгу Форта, которую смог достать, пытаясь ассимилировать Форт, но сейчас я только начинаю ее лучше разбираться. Я хочу предупредить тех, кто придет после, если вы действительно хотите это получить (я обнаружил это слишком поздно), получите книгу по FigForth и реализуйте ее. Коммерческие форты слишком сложны, и самое главное в форте - то, что можно понять всю систему, сверху вниз. Каким-то образом Forth реализует всю среду разработки на новом процессоре, и хотя необходимость в этом, казалось, исчезла с C во всем, он по-прежнему полезен в качестве обряда перехода к написанию Forth с нуля. . Итак, если вы решите сделать это, попробуйте книгу FigForth - это несколько Forth, реализованных одновременно на разных процессорах. Этакий Розеттский камень Фортов.

Зачем нужен стек - эффективность, оптимизация, нулевая точка, сохранение регистров при прерывании, а для рекурсивных алгоритмов это «правильная форма».

person MicroservicesOnDDD    schedule 05.06.2020