- PVSM.RU - https://www.pvsm.ru -
Продолжение статьи [1], где удалось продемонстрировать, что фронтенд [2] стековой машины вполне позволяет спрятать за ним суперскалярный [3] процессор с OoO [4].
Тема данной статьи — вызов функций.
Из общих соображений у описываемой архитектуры ожидаются проблемы с вызовом функций. В самом деле, после возврата из функции мы ожидаем и возврата состояния регистров в контексте текущей функции. В современных регистровых архитектурах для этого регистры делятся [5] на две категории — за сохранность одних отвечает вызывающая сторона, других — вызываемая.
Но в нашей архитектуре фронтенд стековый, следовательно, компилятор может и не знать о существовании каких бы то ни было регистров. А процессор сам должен позаботиться о сохранении/восстановлении контекста, что кажется нетривиальной задачей.
Но прежде сделаем лирическое отступление на тему стека как такового.
Само понятие стек способно вводить в заблуждение.
Вот в IBM/360 [6] нет аппаратного стека. Но функции (в том числе и рекурсивно) вызывать можно, для этого параметры сохраняются в области памяти, которую перед вызовом необходимо выпрашивать у ОС.
В x86 есть аппаратный стек, но никто не относит эту архитектуру к стековым. Этот стек является отличным механизмом для хранения локальных переменных и параметров функций.
AMD29K, SPARC и Itanium относятся к так называемому Berkeley Risc [7] семейству архитектур, у них стек выполняет еще одну важную функцию: пул регистров является верхушкой стека (register windows), что, как предполагается, ускоряет передачу параметров при вызове функций.
SPARC V7 появился на пару лет раньше AMD29K, но он видится (автору) менее архитектурно стройным.
RSE [8] блок Itanium’а в целом аналогичен таковому от AMD29K, но появился существенно позже.
В нём два аппаратных стека. Несколько стеков в архитектуре не новы, это было еще на Burroughs B5000 [10], советских (да и нынешних [11]) Эльбрусах. Но там второй стек предназначен для хранения адресов возврата из процедур. Здесь же они оба используются для хранения данных:
sub gr1,gr1,16 ;function prologue, lr0+lr1+2 local variables asgeu SPILL,gr1,rab ;compare with top of window . . . ;function body jmpi lr0 ;return asleu FILL,lr1,rfb ;compare with bottom of window gr127
Какие интересные идеи следует здесь отметить?
Вот это разделение стека на “большой, но медленный” и “маленький, но быстрый” весьма важно. Разберемся с мотивами.
Идея стека как хранилища локальных переменных (и параметров) прекрасна в своей логичности и завершенности. Слабое место — производительность системы упирается в задержки и производительность памяти. Во времена PDP-11 [15] с этим ничего нельзя было поделать, но с тех пор ситуация изменилась.
Во-первых, доступ к регистрам стал существенно быстрее доступа к памяти, что вызвало потребность в кэшировании данных. Во вторых, появилась возможность иметь гораздо большее к-во регистров.
Обладание большим количеством регистров порождает соблазн воспользоваться ими для ускорения передачи аргументов при вызове функций. В самом деле, параметров обычно немного (меньше, чем локальных данных), их значения почти всегда кому-то нужны. А что из локальных данных заслуживает попадания в регистры пусть решит оптимизация. Это, конечно, очень грубое упрощение, призванное лишь продемонстрировать общую мотивацию.
В данный момент распространенными являются два метода передачи параметров через регистры:
С точки зрения потенциальной производительности оба подхода примерно равносильны. В первом подходе вся тяжесть оптимизации ложится на компилятор, а не на процессор, что (вероятно) облегчает и удешевляет разработку конечной системы.
Но мы несколько увлеклись, пора вернуться к сохранению контекста текущей функции в проектируемой архитектуре.
А что входит в этот контекст? Занятые на момент вызова дочерней процедуры регистры.
При этом регистры недовычисленных выражений связаны между собой через топологическую сортировку, но для вызываемой функции это не имеет значения. На момент захвата моп’ами выходных регистров уже неважно в каком порядке они были захвачены.
Есть нюанс, который стоит отметить — до начала вызова функции должны отработать все мопы, с помощью которых вычислялись её аргументы. Поэтому с точки зрения процессора, вызов функции — обобщенная инструкция с произвольным количеством аргументов.
Теперь стоит определиться с нумерацией регистров.
Пусть нумерация общая, т.е. мы пошли по пути MIPS, а не SPARC.
Попробуем регистровые окна.
Однако, сосредоточившись на внешней стороне вызова функций мы упустили из вида то, как всё это будет исполняться внутри. Автор не считает себя специалистом по “железу”, но проблемы всё же предвидит.
К счастью, разбираться с ними мы будем в следующей статье.
Автор: zzeng
Источник [18]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/stek/115753
Ссылки в тексте:
[1] статьи: https://habrahabr.ru/post/278575/
[2] фронтенд: http://www.ixbt.com/cpu/cpu-pedia.shtml#front-end
[3] суперскалярный: http://www.ixbt.com/cpu/cpu-pedia.shtml#superscal
[4] OoO: http://www.ixbt.com/cpu/cpu-pedia.shtml#OoO
[5] делятся: https://habrahabr.ru/post/267771/
[6] IBM/360: http://everything2.com/title/System%252F370+S-type+linkage+convention
[7] Berkeley Risc: https://en.wikipedia.org/wiki/Berkeley_RISC
[8] RSE: http://people.cs.pitt.edu/~mock/cs2210/handouts/itanium-register-convention.pdf
[9] AMD29K: http://datasheets.chipdb.org/AMD/29K/29kprog.pdf
[10] Burroughs B5000: http://www.cs.virginia.edu/brochure/images/manuals/b5000/descrip/descrip.html
[11] нынешних: http://www.mcst.ru/doc/book_121130.pdf
[12] SPARC: http://people.cs.clemson.edu/~mark/subroutines/sparc.html
[13] trap: https://en.wikipedia.org/wiki/Trap_(computing)
[14] сохраняются: http://ieng9.ucsd.edu/~cs30x/sparcstack.html
[15] PDP-11: https://en.wikipedia.org/wiki/PDP-11
[16] MSVC(x86-64): https://msdn.microsoft.com/en-us/library/9z1stfyw.aspx
[17] i960: http://download.intel.com/design/i960/MANUALS/27248302.pdf
[18] Источник: https://habrahabr.ru/post/279123/
Нажмите здесь для печати.