Стек. Аппаратурные аспекты: от команд процессора до встречных стеков

Модуль "Стек" • урок 3 из 8 • ~8 минут

Суть урока. Стек настолько важен, что поддержан аппаратно: у процессора есть регистр указателя стека и команды PUSH/POP/CALL/RET для вызова подпрограмм. Существуют и полностью стековые (нуль-адресные) машины - Эльбрус, Burroughs, где код получается в 3-4 раза короче. А два стека в одном массиве удобно размещать навстречу друг другу.
Стек. Аппаратурные аспекты: регистр SP хранит адрес вершины, CALL кладет в стек адрес возврата, RET снимает его обратно
Регистр SP и команды CALL и RET: стек в системе команд процессора

Стек. Аппаратурные аспекты: зачем процессору стек

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

Нуль-адресные машины

В обычном процессоре команда сложения указывает операнды: "сложить регистр A с регистром B". В нуль-адресной машине команда сложения не указывает ничего: она берет два верхних числа со стека, складывает и кладет результат обратно на вершину. Это в точности стековая модель вычислений из прошлого урока, реализованная в железе.

Программы для таких машин получаются в 3-4 раза короче - экономятся биты, которые обычно уходят на адреса операндов. Так сочетается выразительность сложных CISC-архитектур с простотой RISC.

Как стек вызывает функции

Главная работа аппаратного стека в классических машинах - механизм вызова процедур и функций, включая рекурсию.

При вызове функции (команда CALL) процессор автоматически кладет в стек адрес возврата - адрес следующей команды. Туда же помещаются переданные параметры и выделяется место под локальные переменные. Когда функция завершается (команда RET), процессор снимает адрес возврата со стека и продолжает выполнение с того места, откуда функцию вызвали.

Вложенные вызовы просто наращивают стек, а возвраты сворачивают его в обратном порядке - LIFO идеально совпадает с логикой "кто последним вызван, тот первым завершится".

Встречные стеки

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

Решение - расположить стеки навстречу друг другу: дно первого - в начале массива (индекс 0), он растет вправо; дно второго - в конце массива (индекс N-1), он растет влево.

Встречные стеки: первый растет от начала общего массива, второй от конца ему навстречу, между вершинами SP1 и SP2 лежит общая свободная память
Встречные стеки: оба растут к центру общего массива и делят свободное место

Что это дает:

А как в Python? Организовывать встречные стеки руками не приходится - списки растут динамически за счет кучи. Но аппаратный стек незримо работает и тут: каждый вызов функции кладет кадр в системный стек вызовов. Ограничение глубины рекурсии (около 1000 вызовов, RecursionError) - это защита от переполнения того самого аппаратного стека.

Коротко: главное

  • Аппаратная поддержка стека: регистр SP и команды PUSH, POP, CALL, RET.
  • CALL кладет в стек адрес возврата, параметры и локальные переменные; RET снимает их и возвращает управление.
  • Нуль-адресные (стековые) машины - Эльбрус, Burroughs: команды без операндов работают с вершиной стека, код в 3-4 раза короче.
  • Встречные стеки: два стека растут навстречу в одном массиве и делят общую память; переполнение - только при встрече вершин.

Проверьте себя

В каком случае встречные стеки в массиве из N ячеек переполняются?

Как вам урок? Спасибо за отзыв!