Цикл команды -- выборка, декодирование, выполнение, обратная запись

ЦП следует одному и тому же четырёхэтапному процессу для каждой команды, словно бесконечный такт конвейера. В этой лекции мы подробно разберём, что делает каждый из этих четырёх этапов, и с помощью Python смоделируем процесс выполнения команд полноценного ЦП.


Аналогия из жизни: кухня ресторана

Представьте, что вы шеф-повар в ресторане. После того как клиент делает заказ, вы готовите блюдо следующим образом:

  1. Выборка: Возьмите новый талон заказа со стойки и посмотрите, что заказал клиент
  2. Декодирование: Проанализируйте меню -- «Тушёная свинина» требует свиной грудинки, соевого соуса и сахара-леденца; «Овощи, обжаренные в воке» требуют зелени и измельчённого чеснока
  3. Готовка (Выполнение): Начните готовить по-настоящему -- нарежьте овощи, обжарьте в воке и приправьте
  4. Подача (Обратная запись): Разложите готовое блюдо по тарелкам и передайте официанту, чтобы он подал

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

Этот четырёхшаговый цикл — основной рабочий ритм CPU. Каждую секунду с момента включения компьютера до момента выключения CPU непрерывно повторяет цикл «fetch-decode-execute-write-back» миллиарды раз в секунду.


Подробное объяснение четырёх этапов

Этап 1: Выборка

Сначала CPU «выбирает» из памяти следующую команду для выполнения.

There is a special register calledсчётчик команд (PC, Program Counter), который всегда хранит адрес следующей команды в памяти.

Что делает этап выборки:

  1. CPU помещает адрес из PC на адресную шину
  2. CPU отправляет сигнал «чтения» по шине управления
  3. Память возвращает данные команды по этому адресу
  4. Команда помещается в регистр команд (IR)
  5. 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. Нажмите кнопку «Далее», чтобы увидеть, какие аппаратные компоненты используются на каждой из четырёх стадий команды; активированные компоненты выделяются цветом соответствующей стадии.

Внутренняя структура CPU и пошаговая демонстрация цикла команды (пример)

Выборка команды IF
Декодирование ID
Выполнение EX
Запись результата WB
Нажмите «Далее», чтобы запустить демонстрацию цикла команды и увидеть, какие компоненты внутри CPU активируются на каждой стадии.
Другие расширения