Цикл команды -- выборка, декодирование, выполнение, обратная запись
ЦП следует одному и тому же четырёхэтапному процессу для каждой команды, словно бесконечный такт конвейера. В этой лекции мы подробно разберём, что делает каждый из этих четырёх этапов, и с помощью Python смоделируем процесс выполнения команд полноценного ЦП.
Аналогия из жизни: кухня ресторана
Представьте, что вы шеф-повар в ресторане. После того как клиент делает заказ, вы готовите блюдо следующим образом:
- Выборка: Возьмите новый талон заказа со стойки и посмотрите, что заказал клиент
- Декодирование: Проанализируйте меню -- «Тушёная свинина» требует свиной грудинки, соевого соуса и сахара-леденца; «Овощи, обжаренные в воке» требуют зелени и измельчённого чеснока
- Готовка (Выполнение): Начните готовить по-настоящему -- нарежьте овощи, обжарьте в воке и приправьте
- Подача (Обратная запись): Разложите готовое блюдо по тарелкам и передайте официанту, чтобы он подал
Каждый заказ проходит через эти четыре шага. Закончив одно блюдо, возьмите следующий заказ. CPU работает так же — выбирает команду, декодирует команду, выполняет команду и записывает результат обратно, в бесконечном цикле.
Этот четырёхшаговый цикл — основной рабочий ритм CPU. Каждую секунду с момента включения компьютера до момента выключения CPU непрерывно повторяет цикл «fetch-decode-execute-write-back» миллиарды раз в секунду.
Подробное объяснение четырёх этапов
Этап 1: Выборка
Сначала CPU «выбирает» из памяти следующую команду для выполнения.
There is a special register calledсчётчик команд (PC, Program Counter), который всегда хранит адрес следующей команды в памяти.
Что делает этап выборки:
- CPU помещает адрес из PC на адресную шину
- CPU отправляет сигнал «чтения» по шине управления
- Память возвращает данные команды по этому адресу
- Команда помещается в регистр команд (IR)
- PC автоматически увеличивается на 1, указывая на следующую команду
Здесь есть ключевая особенность:PC автоматически увеличивается. Это означает, что по умолчанию CPU выполняет инструкции по порядку, если только не встретит инструкцию перехода (например, if-else или циклы), которая изменяет значение PC.
Этап 2: Декодирование
Выбранная инструкция — это строка двоичных цифр (например,0001 0010 0011), CPU необходимо «перевести» её.
Основная работа этапа декодирования:
- Определить код операции (Opcode): Первые несколько битов инструкции сообщают CPU, что делать -- это сложение, вычитание, загрузка данных или переход?
- Извлечь операнды: Оставшиеся биты задают операнд -- какой регистр использовать? Какой адрес в памяти?
- Установить управляющий сигнал: На основе результата декодирования контроллер выдаёт соответствующие управляющие сигналы компонентам, таким как ALU, регистровый файл и шина.
Например, инструкцияADD R1, R2, R3, после декодирования CPU знает: поручить ALU выполнить операцию сложения с входными данными из регистров R2 и R3 и сохранить результат в R1.
Этап 3: Выполнение
Это этап, на котором инструкция фактически «выполняет работу». В зависимости от типа инструкции этап выполнения делает разные вещи:
| Тип команды | Что делает фаза выполнения | Задействованное оборудование |
|---|---|---|
| Арифметические операции (ADD, SUB и т. д.) | АЛУ выполняет арифметические операции над операндами | АЛУ, регистровый файл |
| Логические операции (AND, OR и т. д.) | АЛУ выполняет побитовые операции над операндами | АЛУ, регистровый файл |
| Чтение памяти (LOAD) | Доступ к памяти через шину адреса для чтения данных | Шина адреса, шина данных, память |
| Запись в память (STORE) | Обращение к памяти через шину адреса для записи данных | Шина адреса, шина данных, память |
| Переход (JUMP, BRANCH) | Изменить значение PC, чтобы изменить поток выполнения | Контроллер, регистр PC |
Этап 4: Запись обратно
Результат, полученный на этапе выполнения, должен быть "записан" в целевое место.
Целями обратной записи могут быть:
- регистр: сохранить результат операции в регистр общего назначения (наиболее распространённый случай)
- Память: сохранить результат по адресу в памяти
- Регистр флагов: обновить флаги состояния (такие как: равен ли результат нулю, произошло ли переполнение, является ли он отрицательным и т. д.)
После завершения обратной записи цикл выполнения одной инструкции официально заканчивается. Если PC по-прежнему указывает на допустимый адрес инструкции, CPU автоматически возвращается к этапу выборки и начинает следующую инструкцию.
Представление цикла выполнения инструкции на временной шкале
Приведённая ниже временная диаграмма показывает полный процесс последовательного выполнения трёх инструкций:
时钟周期: 1 2 3 4 5 6 7 8 9 10 11 12
指令1: [取指] [译码] [执行] [写回]
指令2: [取指] [译码] [执行] [写回]
指令3: [取指] [译码] [执行] [写回]
↑ 指令1 完成后才开始指令2,完全串行执行
Обратите внимание, что каждая инструкция здесь занимает 4 такта, а три инструкции вместе занимают 12 тактов. Позже, когда мы рассмотрим конвейеризацию, вы увидите, как это время можно кардинально сократить.
Интерактивная демонстрация: симулятор SimpleCPU
Приведённая ниже программа на Python моделирует полный цикл выполнения инструкций минималистичного ЦП. Она поддерживает инструкции LOAD (загрузка значения), ADD (сложение), MUL (умножение), CMP (сравнение + условный переход) и STORE (вывод на печать) и выводит подробные журналы для каждого этапа.
Пример
Симулятор SimpleCPU — полная демонстрация цикла выполнения инструкции
Поддерживаемые инструкции: LOAD, ADD, MUL, CMP+JGT, STORE, HALT
Каждая инструкция проходит через: выборку (IF) → декодирование (ID) → выполнение (EX) → обратную запись (WB)
"""
class SimpleCPU:
"""Обучающий симулятор ЦП, демонстрирующий четыре стадии цикла выполнения инструкции"""
def __init__(self):
# Регистры общего назначения: R0~R3, начальные значения равны 0
self.registers = {'R0': 0, 'R1': 0, 'R2': 0, 'R3': 0}
# Регистр флагов: сохраняет результаты сравнения
self.flags = {'ZERO': False, 'GREATER': False}
# Счетчик команд: указывает на ячейку памяти следующей команды
self.pc = 0
# Регистр команд: хранит текущую выполняемую команду
self.ir = None
# Память: хранит последовательность команд программы
self.memory = []
# Подсчитывает количество выполнений на каждом этапе
self.stats = {'fetch': 0, 'decode': 0, 'execute': 0, 'writeback': 0}
self.total_cycles = 0
# Флаги управления
self.running = False
def load_program(self, instructions):
"""Загрузить список команд в память"""
self.memory = instructions
self.pc = 0
self.running = True
print(f"[Система] Программа загружена, всего {len(instructions)} инструкций")
print(f"[Система] Начальные регистры: {self.registers}")
print("=" * 60)
def fetch(self):
"""Этап 1: Выборка инструкции"""
self.total_cycles += 1
if self.pc >= len(self.memory):
self.running = False
return None
# Прочитать инструкцию, на которую указывает PC, из памяти
instruction = self.memory[self.pc]
self.ir = instruction
old_pc = self.pc
self.pc += 1 # автоинкремент PC
self.stats['fetch'] += 1
print(f"[Цикл {self.total_cycle}] Выборка инструкции (Устройство выборки инструкций)")
print(fPC={old_pc} → прочитать инструкцию по адресу памяти {old_pc})
print(fСодержимое инструкции:\"{instruction}\"")
print(fОбновление PC: {old_pc} → {self.pc})
print(f" IR \"{instruction}\"")
return instruction
def decode(self, instruction):
"""Этап 2: Декодирование (Декодирование инструкции)"""
self.total_cycles += 1
self.stats['decode'] += 1
# Разобрать код операции и операнд
parts = instruction.split()
opcode = parts[0].upper() # код операции
operands = parts[1:] if len(parts) > 1 else [] # операнд
print(f"[Цикл {self.total_cycles}] Декодирование (ID)")
print(fКод операции: {opcode})
print(f" Операнды: {operands}")
# Пояснение значения кода операции
opcode_meaning = {
'LOAD': 'Загрузить непосредственное значение в регистр',
'ADD': 'Сложение регистров',
'MUL': 'Умножение регистров',
'CMP': 'Сравнить значения двух регистров',
'JGT': 'Условный переход (переход, если больше)',
'STORE':'Вывод значения регистра',
'HALT': 'Остановка выполнения',
}
if opcode in opcode_meaning:
print(fЗначение: {opcode_meaning[opcode]})
return opcode, operands
def execute(self, opcode, operands):
"""Этап 3: Выполнение"""
self.total_cycles += 1
self.stats['execute'] += 1
print(f[Цикл {self.total_cycles}] Выполнение (EX))
if opcode == 'HALT':
print(f" HALT: Завершение выполнения программы")
self.running = False
elif opcode == 'LOAD':
# LOAD Rx, value → сохранить непосредственное значение в регистр Rx
reg = operands[0]
value = int(operands[1])
print(f" Операция: загрузить непосредственное значение {value} в регистр {reg}")
print(fЗадействованные компоненты: шина данных (передаёт непосредственное значение {value}))
# Временно сохранить результат; обновить регистры на этапе обратной записи
return ('LOAD', reg, value)
elif opcode == 'ADD':
# ADD Rx, Ry → Rx = Rx + Ry
rx, ry = operands[0], operands[1]
a = self.registers[rx]
b = self.registers[ry]
result = a + b
print(fОперация: АЛУ выполняет {rx}({a}) + {ry}({b}) = {result})
print(f" Компоненты, задействованные: ALU (арифметическое устройство), регистровый файл")
return ('WRITE_REG', rx, result)
elif opcode == 'MUL':
# MUL Rx, Ry → Rx = Rx * Ry
rx, ry = operands[0], operands[1]
a = self.registers[rx]
b = self.registers[ry]
result = a * b
print(fОперация: ALU выполняет {rx}({a}) * {ry}({b}) = {result})
print(f" Компоненты, задействованные: ALU (арифметическое устройство), регистровый файл")
return ('WRITE_REG', rx, result)
elif opcode == 'CMP':
# CMP Rx, Ry → Сравнить два регистра, установить флаги.
rx, ry = operands[0], operands[1]
a = self.registers[rx]
b = self.registers[ry]
self.flags['ZERO'] = (a == b)
self.flags['GREATER'] = (a > b)
print(fОперация: сравнение {rx}({a}) и {ry}({b}))
print(f" Результат: ZERO={self.flags['ZERO']}, GREATER={self.flags['GREATER']}")
print(f" Задействованные компоненты: ALU (компаратор), регистр флагов")
return ('FLAGS', None, None)
elif opcode == 'JGT':
# JGT адрес → Если флаг GREATER истинен, перейти по адресу
target = int(operands[0])
if self.flags['GREATER']:
old_pc = self.pc
self.pc = target
print(fУсловие выполнено (GREATER=True): переход PC {old_pc} → {target})
print(f" Задействованные компоненты: Controller, регистр PC")
else:
print(fУсловие не выполнено (GREATER=False): нет перехода, PC остаётся {self.pc})
return None
elif opcode == 'STORE':
# STORE Rx → Вывести значение регистра
reg = operands[0]
value = self.registers[reg]
print(fОперация: вывод значения регистра {reg})
return ('OUTPUT', reg, value)
return None
def writeback(self, action):
Этап 4: Запись обратно
self.total_cycles += 1
self.stats['writeback'] += 1
if action is None:
print(f[Цикл {self.total_cycles}] Обратная запись (WB) - нет операции)
return
action_type = action[0]
print(f[Цикл {self.total_cycles}] Обратная запись (WB))
if action_type == 'LOAD':
reg, value = action[1], action[2]
old_val = self.registers[reg]
self.registers[reg] = value
print(f" Цель: регистр {reg}")
print(f" Данные: {old_val} → {value}")
print(f" Канал: шина данных → регистровый файл")
elif action_type == 'WRITE_REG':
reg, result = action[1], action[2]
old_val = self.registers[reg]
self.registers[reg] = result
print(f" Цель: регистр {reg}")
print(fДанные: {old_val} → {result})
print(f" Канал: выход АЛУ → регистровый файл (внутренняя шина)")
elif action_type == 'OUTPUT':
reg, value = action[1], action[2]
print(f" Выход: регистр {reg} = {value}")
print(fКанал: регистровый файл → шина данных → интерфейс ввода-вывода)
print(fТекущее состояние регистров: {self.registers})
print(fФлаги: {self.flags})
print("-" * 60)
def run(self):
"""Запустить всю программу, выполняя инструкции одну за другой"""
print("=" * 60)
print(Запуск симулятора SimpleCPU — демонстрация цикла инструкций (пример))
print("=" * 60)
while self.running:
# Цикл из четырёх этапов
instruction = self.fetch() # Этап 1: Выборка
if instruction is None:
break
opcode, operands = self.decode(instruction) # Этап 2: Декодирование
action = self.execute(opcode, operands) # Этап 3: Выполнение
self.writeback(action) # Этап 4: Обратная запись
# Выполнение завершено, вывести статистику
print("\n" + "=" * 60)
print("Выполнение программы завершено! Статистика:")
print(fВсего тактов: {self.total_cycles})
print(fСтадия выборки выполнена: {self.stats['fetch']} раз)
print(fВыполнение стадии декодирования: {self.stats['decode']} раз)
print(fФаза выполнения выполнена: {self.stats['execute']} раз)
print(fСтадия обратной записи выполнена: {self.stats['writeback']} раз)
print(f" Итоговые регистры: {self.registers}")
print(f" Итоговые флаги: {self.flags}")
print(f" Итоговый PC: {self.pc}")
print("=" * 60)
# ===== Запуск демонстрационной программы =====
# Логика программы: вычислить результат 1+2, затем умножить на 3 и сравнить с 10.
# LOAD R0, 1 → R0 = 1
# LOAD R1, 2 → R1 = 2
# ADD R0, R1 → R0 = R0 + R1 = 1 + 2 = 3
# LOAD R2, 3 → R2 = 3
# MUL R0, R2 → R0 = R0 * R2 = 3 * 3 = 9
# CMP R0, R2 → Сравнить R0(9) и R2(3), установить GREATER=True
# STORE R0 → вывести значение R0
# HALT → остановиться
процессор = SimpleCPU()
программа = [
"LOAD R0 1", # R0 = 1
"LOAD R1 2", # R1 = 2
"ADD R0 R1", # R0 = R0 + R1 = 3
"LOAD R2 3", # R2 = 3
"MUL R0 R2", # R0 = R0 * R2 = 9
"CMP R0 R2", # Сравнить 9 и 3, GREATER=True
"JGT 8", # Если GREATER, перейти к инструкции 8
"LOAD R3 0", # (Это не выполнится, потому что JGT пропустил это)
"STORE R0", # Вывод R0 = 9
"HALT", # Останов
]
cpu.load_program(program)
cpu.run()
Приведённый выше код выводит подробные логи для каждого этапа выполнения каждой инструкции, так что вы можете ясно видеть, что процессор делает внутри на каждом такте. Обратите внимание на следующее:
- PC автоматически увеличивается: Каждый раз при выполнении инструкции PC автоматически увеличивается на 1 (этап выборки)
- Условный переход: Комбинация CMP+JGT реализует оператор if -- если R0 > R2, происходит переход к инструкции 8, пропуская инструкцию 7
- Регистры как перевалочные пункты: Все данные сначала должны быть загружены в регистры; АЛУ читает из регистров, вычисляет и записывает результат обратно в регистр
Интерактивная демонстрация: внутренняя структура процессора и пошаговое воспроизведение цикла выполнения инструкции
Ниже приведена упрощённая схема внутренней структуры CPU, нарисованная с помощью vis-network. Нажмите кнопку «Далее», чтобы увидеть, какие аппаратные компоненты используются на каждой из четырёх стадий команды; активированные компоненты выделяются цветом соответствующей стадии.