Базовый CS с нуля
Регистры
В предыдущих уроках ты встречал «R0» и «R1» — именованные слоты, где CPU держит значения во время арифметики. В цикле выборки–декодирования–исполнения ты видел, что счётчик команд — тоже именованный слот, хранящий адрес следующей инструкции.
Эти слоты называются регистрами. Они — рабочая память CPU: значения, над которыми CPU работает прямо сейчас. Без регистров CPU приходилось бы обращаться в основную память за каждым промежуточным значением, что делало бы каждую программу в сотни раз медленнее.
Что именно такое регистры, сколько их у CPU и почему их существование ускоряет программы?
После этого урока ты сможешь дать определение регистра как сверхбыстрой ячейки хранения внутри CPU, объяснить, почему регистры существуют (память далеко и медленная), описать регистры общего назначения и специальные регистры, а также подтвердить, что счётчик команд сам является регистром.
Что такое регистр. Регистр — это ячейка хранения, встроенная непосредственно в чип CPU, а не в отдельные чипы памяти (ОЗУ). Каждый регистр хранит ровно одно значение: на 64-битном CPU один регистр вмещает 64-битное (8-байтовое) значение.
Регистры сделаны из тех же транзисторных схем, что и логика внутри CPU. Они находятся на той же пластине кремния, в миллиметрах или даже микрометрах от АЛУ, выполняющего арифметику. Доступ к регистру занимает один или два такта — примерно долю наносекунды на современном CPU.
Основная память (ОЗУ), напротив, — это отдельный чип, соединённый шиной. Доступ к ОЗУ занимает 50–100 наносекунд — примерно в 100–300 раз дольше, чем доступ к регистру. Для повторяющихся вычислений эта разница огромна.
Почему существуют регистры: память далеко и медленная. Представь цикл, складывающий 1 миллион чисел. Если бы CPU должен был брать каждую промежуточную сумму из ОЗУ, обновлять её и записывать обратно, каждое сложение оплачивало бы стоимость доступа к памяти ~100 нс. Только на трафик памяти ушло бы 100 мс.
Вместо этого CPU загружает текущую сумму в регистр один раз, прибавляет каждое новое число из ОЗУ к регистру и записывает итоговую сумму обратно в ОЗУ один раз в конце. Регистр хранит промежуточное значение на скорости CPU. Дорогостоящий доступ к ОЗУ происходит только дважды (один раз для загрузки начальной суммы, один раз для записи финального результата).
Это ключевой принцип: доставить данные в «окрестности» CPU, работать с ними там, затем отправить результат обратно. Регистры — это «окрестности».
Регистры общего назначения. Большинство регистров CPU — это регистры общего назначения (РОН, по-английски GPR — General-Purpose Register). У них нет фиксированного смысла — любая инструкция может использовать любой РОН для хранения любого значения. Программист (или компилятор) решает, какой регистр хранит какое значение в данный момент.
Реальные CPU имеют фиксированное количество РОН:
- CPU x86-64 имеет 16 64-битных регистров общего назначения: RAX, RBX, RCX, RDX, RSI, RDI, RSP, RBP и R8–R15.
- CPU ARM (AArch64) имеет 31 64-битный регистр общего назначения: X0–X30.
- Упрощённые учебные CPU (например, в nand2tetris) могут иметь всего несколько: R0, R1, R2, R3.
Количество невелико и фиксировано конструкцией чипа. Если программе нужно больше живых значений, чем есть регистров, компилятор должен временно «выгружать» некоторые значения в память и загружать их обратно при необходимости — неизбежная цена, которую компиляторы стараются минимизировать.
Почему это работает
Зачем не добавить больше регистров? Добавление регистров означает больше транзисторов на чипе, что увеличивает потребление энергии и тепловыделение. Что ещё важнее, кодировка каждой инструкции должна указывать, какие регистры она использует; больше регистров требует больше битов в инструкции для их наименования. 4-битное поле регистра в инструкции называет 16 регистров, 5-битное — 32. Более широкие инструкции означают больший размер кода в памяти. Здесь действует закон убывающей отдачи: переход с 4 на 16 регистров даёт большой выигрыш; с 64 на 128 — значительно меньший, потому что компиляторы уже эффективно справляются с 64. Большинство архитектур останавливаются на 16–32 РОН.
Специальные регистры. Не все регистры являются регистрами общего назначения. CPU также имеет несколько специальных регистров с выделенной функцией в аппаратуре:
-
Счётчик команд (СК, PC) — как ты узнал в предыдущем уроке, СК хранит адрес следующей инструкции для выборки. CPU обновляет его автоматически после каждой инструкции. Инструкция JUMP записывает новое значение в СК. Это регистр, как и любой другой, но он недоступен для общего использования.
-
Указатель стека (SP, Stack Pointer) — хранит адрес вершины стека вызовов в памяти. Операции push и pop автоматически изменяют указатель стека. Со стеком вызовов ты познакомишься в более позднем разделе.
-
Регистр флагов (Status Register) — набор однобитных флагов, фиксирующих результат последней операции: был ли результат равен нулю, был ли перенос из старшего бита (переполнение), был ли результат отрицательным и т. д. Инструкции условного JUMP проверяют флаги, чтобы решить, выполнять ли переход.
-
Регистр инструкций (РИ, IR) — хранит текущую выполняемую инструкцию после её выборки из памяти, как ты видел в цикле выборки–декодирования–исполнения.
Частая ошибка
Распространённая путаница: воспринимать регистры и память (ОЗУ) как две версии одного и того же, отличающиеся только скоростью. Они отличаются по природе. ОЗУ — это большой, адресуемый массив байтов, сохраняющий своё содержимое: оно переживает вызовы функций и разделяется между разными программами (через ОС). Регистры — крошечный, именованный, неадресуемый набор слотов, принадлежащих целиком текущему потоку инструкций. Регистр нельзя «адресовать» адресом памяти — он называется напрямую в кодировке инструкции. И когда программа сохраняет состояние в файл или отправляет данные по сети, данные должны пройти через память — они никогда не проходят через регистр напрямую.
Отслеживание состояния регистров через три инструкции.
Начальное состояние: R0 = 0, R1 = 0, R2 = 0, СК = 100.
Память (хранящиеся значения):
- Адрес 200: значение 25
- Адрес 201: значение 17
Инструкция по адресу 100: LOAD R0, 200
- Исполнение: читаем адрес памяти 200, получаем 25. Сохраняем 25 в R0.
- После: R0 = 25, R1 = 0, R2 = 0, СК = 104.
Инструкция по адресу 104: LOAD R1, 201
- Исполнение: читаем адрес памяти 201, получаем 17. Сохраняем 17 в R1.
- После: R0 = 25, R1 = 17, R2 = 0, СК = 108.
Инструкция по адресу 108: ADD R2, R0, R1
- Исполнение: АЛУ вычисляет 25 + 17 = 42. Результат сохраняется в R2.
- После: R0 = 25, R1 = 17, R2 = 42, СК = 112.
Наблюдение: на протяжении всех трёх циклов счётчик команд тоже менялся: 100 → 104 → 108 → 112. Именно он управляет шагом выборки. Остальные регистры изменялись только тогда, когда конкретная инструкция указывала их как цель.
Доступ к регистру занимает примерно 1–2 такта. Доступ к ОЗУ — примерно 100–300 тактов. Во сколько раз быстрее доступ к регистру, чем к ОЗУ (используй нижнюю границу)? Введи число.
CPU x86-64 имеет сколько 64-битных регистров общего назначения? Введи число.
Каждый регистр общего назначения на 64-битном CPU вмещает сколько бит? Введи число.
Какой специальный регистр хранит адрес следующей инструкции для выборки? Введи 1 (счётчик команд) или 2 (регистр флагов).
После выполнения LOAD R0, 200, где оказывается значение из адреса памяти 200? Введи 1 (в регистре R0) или 2 (в регистре R1).
Почему CPU хранит рабочие значения в регистрах, а не читает и записывает основную память при каждом вычислении?
Регистр — сверхбыстрая, именованная ячейка хранения, физически находящаяся внутри чипа CPU. В отличие от основной памяти (ОЗУ), регистры не адресуются адресом памяти — они называются напрямую в кодировке инструкций (R0, R1, RAX и т. д.). Регистры в 100–300 раз быстрее ОЗУ, поскольку находятся на той же кремниевой пластине, что и АЛУ. CPU выполняет свою реальную работу — арифметику, логику, вычисление адресов — над значениями в регистрах, а не над значениями в памяти напрямую. Регистр общего назначения (РОН) может хранить любое значение в любой момент; какой регистр хранит какое значение — решает компилятор. Специальные регистры выполняют фиксированные аппаратные роли: счётчик команд хранит адрес следующей инструкции, указатель стека отслеживает стек вызовов, регистр флагов фиксирует результаты операций. Важно: счётчик команд сам является регистром — инструкция, которая его обновляет, — это обычное обновление СК, происходящее в каждом цикле.