7. Цикл исполнения инструкции. Fetch, Decode, Execute
В предыдущей главе 6. Анатомия CPU. Datapath, Control Unit и Register File мы изучили устройство центрального процессора в статике: познакомились с регистровым файлом, вычислительным трактом данных (Datapath) и дирижером оркестра — устройством управления (Control Unit).
Теперь пришло время вдохнуть в этот кремниевый кристалл жизнь и посмотреть на процессор в динамике.
С момента нажатия кнопки питания на сервере и до выключения питания процессор занят ровно одним делом. Он выполняет один и тот же монотонный, математически строгий алгоритм, повторяющийся такт за тактом со скоростью несколько миллиардов раз в секунду:
$$\text{Fetch (Выборка)} \longrightarrow \text{Decode (Декодирование)} \longrightarrow \text{Execute (Выполнение)}$$
Этот неумолимый кремниевый пульс называется циклом исполнения инструкции (Instruction Execution Cycle). Давайте разберем каждый его этап под микроскопом, заглянем в физику задержек и выясним, как высокоуровневый рантайм Go умудряется взаимодействовать с этим аппаратным циклом на лету.
1. Fetch (Выборка инструкции)
Процессор не обладает человеческим сознанием и не может «охватить взглядом» вашу программу на Go целиком. Для него не существует ни пакетов, ни структур, ни модулей. В каждый отдельный миг времени кругозор ядра сужен до одной-единственной машинной команды, на которую указывает его главный ориентир.
В архитектуре x86-64 этот ориентир хранится в регистре RIP (64-bit Instruction Pointer), а в ARM64 — в регистре PC (Program Counter).
flowchart LR
RIP["Счетчик команд (RIP)"] --> Bus["Шина адреса"]
Bus --> L1i["Кэш инструкций L1i<br>(~1 ns, SRAM)"]
L1i -- "Промах (Miss)" --> L2["Кэш L2 / L3"]
L2 -- "Промах (Miss)" --> RAM["Оперативная память (RAM)<br>(~60 ns, DRAM)"]
L1i -- "Попадание (Hit)" --> IR["Instruction Register (IR)<br>Сырые байты команды"]
RIP -.->|"Аппаратный инкремент (+ len)"| RIP_Next["Следующий RIP"]
classDef fast fill:#1e293b,stroke:#10b981,stroke-width:1px,color:#f8fafc;
classDef slow fill:#1e293b,stroke:#f59e0b,stroke-width:1px,color:#f8fafc;
classDef core fill:#0f172a,stroke:#38bdf8,stroke-width:1px,color:#f8fafc;
class L1i,IR fast;
class L2,RAM slow;
class RIP,Bus,RIP_Next core;
На фазе Fetch происходит следующая цепочка физических событий:
- Control Unit выставляет число из регистра
RIPна шину адреса памяти. - Процессор пытается прочитать инструкцию. Первым делом запрос попадает в выделенный аппаратный кэш инструкций первого уровня (L1i). Он построен на сверхбыстрых SRAM-триггерах и отдает данные за несколько тактов CPU (обычно 3–5, около 1 нс; в конвейере эта задержка частично скрывается).
- Прочитанные байты машинного кода (например, шестнадцатеричная тройка
48 01 D8) защелкиваются во внутреннем буфере ядра — Instruction Register (IR) (так устроена учебная модель; в реальных x86-ядрах отдельного архитектурного регистраIRнет, а байты проходят через буфер выборки и декодер). - Аппаратный сумматор инкрементирует значение
RIP, прибавляя к нему длину прочитанной инструкции, чтобы подготовить ядро к следующему такту.
2. Decode (Декодирование)
Сырые байты загружены в Instruction Register, но для вычислительного блока (Datapath) они пока остаются слепым набором битов. Набор байтов необходимо расшифровать и превратить в скоординированные электрические сигналы управления.
Эту задачу решает Instruction Decoder (Декодер инструкций) — сложнейшая комбинационная схема внутри Control Unit.
На этапе Decode:
- Декодер анализирует код операции (Opcode). Увидев байты
48 01 D8, декодер распознает:- Префикс
48— операция над 64-битными регистрами; - Опкод
01— операция сложения (ADD); - Байт
D8— операндыRAXиRBX(сложениеADD RAX, RBX).
- Префикс
- Декодер определяет, какие именно регистры нужны для вычислений, и подает управляющий сигнал
Readна соответствующие порты регистрового файла (Register File). - Настраиваются аппаратные мультиплексоры (MUX), коммутирующие шины между регистрами и входами ALU.
- На управляющие входы ALU подается бинарный код команды
ADD.
3. Execute (Выполнение)
Все подготовительные цепи скоммутированы, операнды выставлены на входные шины. Наступает момент физического совершения полезной работы.
На этапе Execute:
- Вычисление: Арифметико-логическое устройство (ALU) выполняет команду. Например, при сложении операндов
RAXиRBXкремниевые сумматоры Carry-Lookahead прогоняют биты через каскады вентилей за доли наносекунды. - Обновление флагов: По результату операции ALU выставляет признаки в регистре состояния
RFLAGS(Zero FlagZF— равен ли результат нулю, Carry FlagCF— произошел ли перенос, Overflow FlagOF— случилось ли знаковое переполнение). - Запись результата (Writeback): Полученная сумма по шине данных направляется обратно в регистровый файл и по сигналу
Write Enableзащелкивается в целевом регистреRAX. - Ветвления и прыжки: Если выполнялась инструкция ветвления (например, безусловный
JMP, условный переход или вызов функцииCALL), то на этапе Execute результат вычисления адреса принудительно перезаписывает счетчикRIP. Линейный ход программы нарушается, и следующий цикл Fetch начнется с нового адреса в памяти.
flowchart TD
Start["Начало такта"] --> Fetch["1. Fetch (Выборка)<br>Чтение байтов по адресу RIP<br>Инкремент RIP = RIP + len"]
Fetch --> Decode["2. Decode (Декодирование)<br>Анализ Opcode (напр. ADD RAX, RBX)<br>Подача сигналов Read на регистры"]
Decode --> Execute["3. Execute (Выполнение)<br>ALU вычисляет результат<br>Обновление флагов RFLAGS"]
Execute --> WB["4. Writeback (Запись)<br>Защелкивание ответа в регистр RAX<br>или запись в кэш данных L1d"]
WB --> Jump{"Инструкция перехода<br>(JMP, CALL, RET)?"}
Jump -->|"Да: ветвление"| UpdateRIP["Принудительная перезапись RIP<br>новым целевым адресом"]
Jump -->|"Нет: линейный код"| Start
UpdateRIP --> Start
classDef step fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc;
classDef branch fill:#1e293b,stroke:#f59e0b,stroke-width:1px,color:#f8fafc;
class Fetch,Decode,Execute,WB step;
class Jump,UpdateRIP branch;
Mechanical Sympathy: Go Runtime и цикл процессора
Понимание непрерывного цикла «Fetch-Decode-Execute» позволяет блестяще ответить на один из самых классических вопросов для Senior Go-разработчиков: как устроена невытесняющая и вытесняющая многозадачность?
sequenceDiagram
autonumber
participant CPU as Ядро CPU (Fetch-Decode-Execute)
participant Spin as Горутина с for {}
participant Sysmon as Фоновый поток sysmon
participant Kernel as Ядро Linux
participant Runtime as Рантайм Go (sighandler)
CPU->>Spin: Непрерывное исполнение цикла: JMP -> JMP -> JMP
Note over Sysmon: Горутина крутится > 10 мс без прерываний!
Sysmon->>Kernel: tgkill(tid, SIGURG)
Kernel->>CPU: Доставка сигнала (Signal Delivery)
Note over CPU: Разрыв цикла Fetch-Decode-Execute!<br>Сохранение RIP в стек
CPU->>Runtime: Переход в обработчик runtime.sighandler
Runtime->>Runtime: Эмуляция вызова: прерванный RIP на стек,<br>новый RIP = runtime.asyncPreempt
Runtime-->>CPU: Возврат из сигнала
CPU->>Runtime: Исполнение asyncPreempt: сохранение регистров,<br>вытеснение горутины в очередь
CPU->>CPU: Планировщик переключается на функцию main() -> "Готово!"
Именно так глубокое понимание аппаратного цикла инструкций и механизма сигналов превращает загадочную магию рантайма Go в абсолютно прозрачный инженерный механизм.
Итог
- Fetch (Выборка): Ядро считывает байты машинной команды по адресу из регистра
RIP(илиPC), загружает их в регистр инструкцииIRи инкрементирует счетчик адреса. Промахи в кэше инструкций L1i приводят к простоям конвейера на сотни тактов. - Decode (Декодирование): Комбинационный дешифратор команд разбирает опкод (например,
48 01 D8), подготавливает входы ALU и выставляет сигналыReadна регистрах операндов. В архитектуре x86 переменная длина команд (1–15 байт) сильно усложняет декодер по сравнению с фиксированным 4-байтным форматом ARM64. - Execute (Выполнение) и Writeback: ALU физически перемалывает биты, выставляет флаги состояний
RFLAGSи сохраняет ответ обратно в регистровый файл по сигналуWrite Enable. - Инструкции переходов и ветвления (
if/else,JMP,CALL,RET): Ломают последовательный шаг $RIP = RIP + \text{len}$ и принудительно загружают в счетчик команд новый адрес в памяти. - Асинхронное вытеснение в Go (1.14+): В Linux плотный бесконечный цикл
for {}прерывают, послав потоку сигналSIGURG. (На Windows используютсяSuspendThread/SetThreadContext; есть и отладочная настройкаGODEBUG=asyncpreemptoff=1, отключающая асинхронное вытеснение; синхронное вытеснение через проверку stack guard в прологах функций работает всегда.)
Мы разобрались, как процессор читает, декодирует и выполняет машинные команды. Но кто определяет сам словарь этих команд? Откуда компилятор Go знает, что сложение 64-битных регистров — это именно байты 48 01 D8, а не какие-то другие?
Ответ на этот вопрос дает великий архитектурный контракт между создателями кремния и разработчиками компиляторов. В следующей статье мы изучим: 8. ISA. Интерфейс между железом и софтом.