Основы Computer Science Как работает процессор: фон Нейман, регистры, ALU, fetch-decode-execute
0%

Как работает процессор: фон Нейман, регистры, ALU, fetch-decode-execute

Как работает процессор: фон Нейман, регистры, ALU, fetch-decode-execute

В предыдущей статье трека мы собрали из логических вентилей сумматор и триггер и увидели, что из AND, OR и NOT можно построить схему, которая складывает числа и запоминает бит (Булева логика и вентили). Но сумматор сам по себе — это не компьютер. Он умеет ровно одно и делает это, пока на входах держат напряжение. Чтобы получить машину, которая читает программу и выполняет тысячи разных операций в нужном порядке, между вентилями и кодом нужен ещё один слой — процессор.

Процессор — это самая недопонятая деталь всего стека. Сверху его видят как «то, что исполняет мой код», снизу — как «набор транзисторов». Между этими двумя описаниями лежит удивительно простая идея, которую в 1945 году записал Джон фон Нейман и которая до сих пор лежит в основе почти каждого чипа: и программа, и данные — это просто числа в одной памяти, а процессор в цикле достаёт очередное число-команду и выполняет его. Разберём эту идею по слоям: сначала общая схема машины, потом её части (регистры, АЛУ, устройство управления), потом как команда превращается в биты, и наконец сам пульс — цикл fetch-decode-execute. В конце честно посмотрим, где эта модель протекает, потому что настоящий Core i9 отличается от неё примерно как реактивный самолёт от бумажного.

Модель фон Неймана: одна память на всё

До фон Неймана программу часто задавали физически — перекоммутацией проводов (ENIAC «программировали» неделями, перетыкая кабели). Прорыв был концептуальный: если команды закодировать теми же числами, что и данные, и положить в ту же память, то программу можно загрузить как данные, менять на лету и хранить рядом с тем, над чем она работает. Это принцип хранимой программы, и вся современная вычислительная техника — его прямое следствие.

Из этого принципа вырастает классическая схема из пяти блоков.

Архитектура фон Неймана: команды и данные в одной памяти

  • Память (RAM) — единое адресное пространство пронумерованных ячеек. В одних лежат команды, в других данные, и физически они неотличимы: и то и другое — байты. Подробно про устройство и скорость памяти — в следующей статье (Иерархия памяти).
  • АЛУ (ALU) — арифметико-логическое устройство, тот самый сумматор из прошлой статьи, доросший до вычитания, AND/OR/XOR, сдвигов и сравнений.
  • Устройство управления (CU) — дирижёр: читает очередную команду, понимает, что она значит, и раздаёт остальным блокам управляющие сигналы в правильном порядке.
  • Регистры — крошечная сверхбыстрая память внутри процессора, где хранятся числа, с которыми он работает прямо сейчас.
  • Ввод-вывод — связь с внешним миром (клавиатура, экран, диск, сеть).

Соединяют всё это шины — пучки проводов: по шине адреса процессор говорит «дай мне ячейку номер N», по шине данных число едет туда-обратно, по шине управления идут сигналы «читаю / пишу». АЛУ и CU вместе с регистрами и образуют процессор (CPU); память и устройства — снаружи.

У единой памяти есть оборотная сторона, которую фон Нейман создал и которую его именем же и называют — бутылочное горлышко фон Неймана: команды и данные ходят по одной шине, и процессор часто простаивает, ожидая, пока из памяти приедет очередное слово. Вся иерархия кэшей (статья 06) существует именно чтобы это горлышко расширить. Альтернатива — гарвардская архитектура с раздельными памятью и шинами для команд и данных — живёт внутри микроконтроллеров и в кэшах реальных CPU (раздельные L1i и L1d), но снаружи программист почти всегда видит фон-неймановскую картину.

Регистры: рабочий стол процессора

Регистр — это несколько триггеров, хранящих слово (обычно 32 или 64 бита). Он находится буквально внутри ядра, поэтому доступ к нему занимает часть одного такта — на порядки быстрее любой памяти. Аналогия: регистры — это то, что лежит у вас в руках, RAM — шкаф в этой же комнате, диск — склад на другом конце города. Работать можно только с тем, что в руках; всё остальное надо сначала принести.

Регистров мало (в x86-64 — шестнадцать 64-битных регистров общего назначения) и они бывают двух сортов: общего назначения, куда программа кладёт любые числа, и специальные, у каждого из которых своя роль в работе самого процессора:

Регистр Назначение Что было бы без него
PC / IP (счётчик команд) адрес следующей команды процессор не знал бы, что выполнять дальше
IR (регистр команды) текущая команда, только что прочитанная из памяти нечего было бы декодировать
SP (указатель стека) вершина стека вызовов не работали бы вызовы функций и локальные переменные
FLAGS (регистр флагов) результаты сравнений: ноль, перенос, знак, переполнение не было бы if и циклов
ACC / R0…Rn операнды и результаты вычислений АЛУ негде было бы взять числа

Ключевые — PC и IR. PC хранит адрес следующей команды и после каждой команды увеличивается (или прыгает при переходе); IR держит команду, которую процессор разбирает прямо сейчас. Эти два регистра и есть материальное воплощение идеи «хранимой программы»: PC листает память как книгу, IR — открытая страница.

FLAGS заслуживает отдельного слова: именно он делает компьютер способным ветвиться. АЛУ, вычисляя a - b, попутно выставляет флаг «ноль» (если равны) и флаг «знак» (если результат отрицателен). Команда условного перехода потом просто смотрит на эти флаги и решает, менять PC или нет. Весь if, весь while, вся логика вашей программы на нижнем уровне — это «вычесть, посмотреть на флаг, при нужном значении прыгнуть».

АЛУ: где происходит арифметика

Арифметико-логическое устройство — комбинационная схема (без памяти) с двумя входами для операндов, входом кода операции («что делать») и выходом результата плюс флагами. Внутри — сумматор, схемы побитовых AND/OR/XOR/NOT, сдвигатель (barrel shifter) и компаратор. Умножение и деление в простых процессорах делаются серией сложений и сдвигов; в современных — отдельными аппаратными блоками.

Важная деталь, которая многих удивляет: вычитание, сравнение и логика ветвления — это одна и та же операция. Процессор вычитает через сложение с дополнительным кодом (a - b = a + (~b + 1)), а сравнение a == b — это то же вычитание, у которого нас интересует не результат, а только флаг «ноль». Поэтому у АЛУ нет отдельной «схемы сравнения»: она переиспользует сумматор. Как именно отрицательные числа помещаются в биты через дополнительный код — разбирается в статье Представление данных.

Как команда становится числами

Раз команды хранятся в памяти как числа, у каждой должна быть кодировка. Формат команды делит биты слова на поля: опкод (код операции — что делать) и операнды (над чем). Устройство управления, получив команду в IR, первым делом смотрит на опкод и по нему понимает, как читать остальные биты.

Формат 16-битной команды: опкод и операнды

Возьмём игрушечную систему команд. Команда ADD R1, R2 («сложи R1 и R2, результат в R1») кодируется так: 4 бита опкода 0010, 3 бита приёмника 001 (это R1), 3 бита источника 010 (R2), остальное не используется. А LOADI R1, 7 («положи число 7 прямо в R1») — тот же 16-битный узор, но опкод 0001, и оставшиеся биты читаются уже не как номер регистра, а как непосредственное значение (immediate) 7. Один и тот же набор нулей и единиц значит разное — всё решает опкод.

Набор всех допустимых команд и правил их кодирования называется архитектурой системы команд (ISA) — x86-64, ARM, RISC-V. ISA — это контракт между железом и всем софтом: пока процессор честно исполняет команды по правилам ISA, ему всё равно, кто их сгенерировал — ассемблер, компилятор C или JIT виртуальной машины. Как из вашего кода на языке высокого уровня получается этот поток команд, подробно разбирает статья От кода к исполнению.

Люди не пишут опкоды в двоичном виде — для этого есть ассемблер: мнемоники ADD, LOAD, JMP один-к-одному соответствуют машинным командам. Вот наша программа «7 + 3» на ассемблере игрушечной машины:

        LOADI R1, 7     ; R1 <- 7            (опкод 0001)
        LOADI R2, 3     ; R2 <- 3
        ADD   R1, R2    ; R1 <- R1 + R2 = 10 (опкод 0010)
        STORE R1, 0x40  ; память[0x40] <- R1 (записали результат)
        HALT            ; стоп

Пять строк, пять команд, пять чисел в памяти. Теперь посмотрим, как процессор их исполняет.

Цикл выборка-декодирование-исполнение

Вся работа процессора — это один цикл, повторяющийся миллиарды раз в секунду. У него три фазы, давшие название: fetch → decode → execute (выборка → декодирование → исполнение).

Разберём фазы на нашей команде ADD R1, R2 (пусть её адрес в памяти — 0x02):

  1. Выборка. CU выставляет содержимое PC (0x02) на шину адреса и сигнал «чтение». Память отдаёт по шине данных 16-битное слово команды; оно попадает в IR. Сразу же PC := PC + 1 — процессор уже смотрит на следующую команду.
  2. Декодирование. CU читает опкод из IR (0010), понимает «это сложение регистр+регистр», и вытаскивает номера регистров: приёмник 001 (R1), источник 010 (R2). Значения R1 и R2 подаются на входы АЛУ.
  3. Исполнение. АЛУ складывает 7 + 3 = 10, результат 10 записывается обратно в R1, попутно выставляются флаги (не ноль, нет переноса). Цикл начинается заново — с новым PC.

То же самое как протокол между блоками внутри процессора — кто кому что передаёт за один оборот цикла:

Обратите внимание: переход (JMP, ветвление) — это не какая-то особая магия, а просто запись нового значения в PC. Команда JMP 0x10 в фазе исполнения кладёт 0x10 в PC — и следующая выборка пойдёт уже оттуда. Условный переход делает то же самое, но лишь если нужный флаг выставлен. Циклы, if, вызовы функций — всё это в конечном счёте манипуляции с PC.

Такт: пульс машины

Цикл синхронизируется тактовым генератором — кварцем, который выдаёт прямоугольные импульсы с постоянной частотой. Частота 3 ГГц означает 3 миллиарда тактов в секунду; на каждом такте по всему чипу защёлкиваются регистры и схемы делают следующий шаг. Такт нужен, чтобы сигналы успели «устояться»: комбинационная логика (тот же сумматор) даёт правильный ответ не мгновенно — сигнал распространяется через вентили за конечное время, и такт должен быть длиннее самого медленного пути. Отсюда прямая связь: короче такт (выше частота) — быстрее работа, но меньше времени на устаканивание, больше тепла и жёстче предел. Именно поэтому гонка гигагерц остановилась около 4–5 ГГц ещё в середине 2000-х, и производители стали наращивать не частоту, а число ядер — к этому мы вернёмся.

Наивно кажется, что одна команда = один такт. В простых процессорах это близко к правде, но уже здесь видно, что одна команда требует нескольких шагов (выборка, декодирование, несколько под-шагов исполнения), а значит — нескольких тактов. Как процессоры научились «прятать» эти такты, показывает следующий раздел.

Где эта модель протекает

Всё описанное — честная и работающая модель, по которой построены микроконтроллеры и по которой стоит думать о процессоре. Но CPU в вашем ноутбуке отличается от неё радикально, и знать об этих отличиях важно: они объясняют, почему одинаковый по числу операций код может работать в разы медленнее, и откуда взялись целые классы уязвимостей.

Конвейер (pipeline)

Пока команда исполняется, устройство выборки простаивает — и наоборот. Расточительство. Решение позаимствовано у конвейера Форда: разбить обработку команды на стадии и запустить их внахлёст, чтобы на каждой стадии одновременно находилась своя команда.

Классический RISC-конвейер имеет 5 стадий (IF, ID, EX, MEM, WB); каждая команда всё так же проходит их за 5 тактов, но в установившемся режиме на каждом такте завершается одна команда — пятикратный рост пропускной способности без роста частоты. Плата — сложность: если команда зависит от результата предыдущей, которого ещё нет (hazard), конвейер приходится тормозить. А главная беда — ветвления: пока не вычислен условный переход, неизвестно, какую команду выбирать следующей. Отсюда предсказание переходов (branch prediction): процессор угадывает исход if и начинает исполнять предполагаемую ветку заранее (спекулятивно). Угадал — выигрыш; ошибся — конвейер сбрасывается, десятки тактов впустую. Вот почему предсказуемые ветки (отсортированные данные) исполняются быстрее непредсказуемых при том же числе операций.

Суперскалярность и внеочередное исполнение

Дальше — больше. Суперскалярный процессор имеет несколько АЛУ и завершает по несколько команд за такт. Внеочередное исполнение (out-of-order) позволяет, наткнувшись на команду, ждущую данные из памяти, не стоять, а выполнить следующие независимые команды, пока данные едут, — а результаты потом «собрать» в правильном порядке. Снаружи ISA-контракт соблюдён (программа видит последовательное исполнение), внутри — управляемый хаос из сотен команд «в полёте».

Именно спекулятивное и внеочередное исполнение породили в 2018 году Spectre и Meltdown: процессор спекулятивно читал данные, к которым у программы не было прав, отбрасывал результат при откате — но следы оставались в кэше и вычитывались по таймингам. Абстракция «команды исполняются строго по одной и по порядку» протекла на уровне безопасности целой индустрии (подробнее об этом классе атак — в статье Основы безопасности).

Кэши и несколько ядер

Горлышко фон Неймана никуда не делось: обращение в RAM — это сотни тактов, вечность для процессора на 3 ГГц. Между ядром и памятью стоят несколько уровней кэша (L1/L2/L3), и реальная скорость программы часто определяется не числом команд, а тем, попадают ли данные в кэш. Это тема следующей статьи. А поскольку частоту дальше не поднять, производительность растёт вширь — несколькими ядрами, каждое из которых крутит свой цикл fetch-decode-execute. Заставить их работать вместе, не мешая друг другу, — отдельная и трудная задача (Многозадачность и параллелизм). А переключением между программами на одном ядре и обработкой прерываний от устройств заведует операционная система (Что делает ОС): само прерывание на уровне железа — это сигнал, который заставляет процессор бросить текущий цикл, сохранить PC и прыгнуть на код-обработчик.

Немного истории — как усложнялась одна идея

Сквозь все эти слои сложности неизменной остаётся суть 1945 года: достать команду по PC, разобрать, выполнить, повторить. Конвейеры, спекуляция и кэши — это способы делать этот цикл быстрее, не меняя его смысла. Понимаете базовый цикл — понимаете, что оптимизируют все остальные ухищрения.

Типичные заблуждения

  • «Процессор понимает мой код на Python/Java». Нет. Он понимает только машинные команды своей ISA. Всё остальное — работа компиляторов и интерпретаторов (статья 07).
  • «Больше гигагерц = быстрее». Только при прочих равных. Число команд за такт (IPC), кэши, число ядер и предсказуемость кода часто важнее частоты; 3-гигагерцевый чип легко обгоняет 4-гигагерцевый.
  • «Одна команда — один такт». Уже в простой модели команда занимает несколько тактов; конвейер лишь создаёт иллюзию «команда за такт» на пропускной способности.
  • «Регистров можно нарожать сколько угодно». Их мало и это часть ISA: адрес регистра кодируется в команде, больше регистров — длиннее команды, дороже декодер. Компилятор ведёт борьбу за то, чтобы горячие переменные жили в регистрах, а не в памяти.

Мини-итог

Процессор — это машина, реализующая одну простую идею фон Неймана: программа и данные лежат числами в общей памяти, а специальный регистр PC указывает на следующую команду. Внутри — регистры (быстрый рабочий стол), АЛУ (арифметика и логика через один сумматор) и устройство управления (дирижёр). Работа сводится к бесконечному циклу выборка → декодирование → исполнение, синхронизированному тактовым генератором; ветвления, циклы и вызовы — это манипуляции с PC, а if держится на флагах АЛУ. Реальные процессоры прячут за этой моделью конвейеры, спекуляцию, внеочередное исполнение, кэши и множество ядер — и знать, где эта абстракция протекает, полезно и для производительности, и для безопасности. Но ядро всего стека — тот самый трёхтактный цикл, к которому в конце концов сводится любая программа.

Что дальше

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

Иерархия памяти: регистры, кэш, RAM, диск и почему это важно

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов