Базовый CS с нуля
Что такое поток выполнения
Ты уже знаешь, как CPU выполняет цикл выборка–декодирование–исполнение бесконечно. Ты видел счётчик команд — регистр, хранящий адрес следующей инструкции. Но до сих пор все разобранные программы были абсолютно прямолинейны: инструкция 1, затем 2, затем 3, по порядку, без отступлений.
Реальные программы не прямолинейны. Проверка пароля должна делать одно, когда пароль верный, и другое — когда он неверный. Обратный отсчёт должен продолжать вычитать, пока не достигнет нуля. Что-то в программе должно управлять тем, какую инструкцию CPU возьмёт следующей.
Это «что-то» — тема всего раздела. Но прежде чем перейти к ветвлениям и циклам, нужна чёткая ментальная модель случая по умолчанию — прямолинейного выполнения — и понимание того, почему оно работает именно так. Как только ты поймёшь умолчание, любое отклонение от него станет сразу понятным.
После этого урока ты сможешь определить поток выполнения как упорядоченную последовательность инструкций, которые CPU фактически исполняет, объяснить, почему по умолчанию порядок последовательный сверху вниз, описать точно, как счётчик команд создаёт этот прямолинейный порядок, и назвать, какой тип инструкции необходим, чтобы порядок отклонился от прямолинейного.
Поток выполнения: последовательность, которую CPU реально исполняет. Программа, загруженная в память, — это просто список байтов инструкций, лежащих в ячейках. Сами байты не перемещаются и не имеют встроенного порядка — это просто данные по известным адресам.
Поток выполнения — это последовательность, в которой эти инструкции фактически исполняются CPU. Он определяется не самими байтами, а тем, на какую инструкцию счётчик команд указывает в каждый момент.
Это различие важно: можно иметь две одинаковые программы в памяти и запустить их с совершенно разными потоками выполнения, если счётчик команд направлять по-разному. Байты фиксированы; поток — динамичен. Что определяет поток — это то, как движется счётчик команд.
Счётчик команд управляет потоком. Вспомни из раздела 03: счётчик команд (СК, англ. program counter, PC) — это специальный регистр внутри CPU, хранящий адрес памяти следующей инструкции для выборки. После того как CPU выбрал инструкцию, цикл переходит к декодированию, затем к исполнению — и после исполнения СК должен быть обновлён перед следующей выборкой.
Правило обновления по умолчанию простое: прибавить размер текущей инструкции к СК. На архитектуре, где каждая инструкция занимает 4 байта, СК продвигается на 4 после каждой инструкции. Если текущая инструкция была по адресу 100, следующая выборка придёт из адреса 104. Затем 108. Затем 112. CPU шагает вперёд по памяти равными шагами.
В этом обновлении по умолчанию нет никакого решения. Схема, прибавляющая фиксированное смещение к СК, срабатывает автоматически после каждой инструкции без перехода — независимо от того, что вычислила инструкция или какие значения хранят регистры.
Прямолинейное выполнение: как это выглядит в памяти. Когда программа загружается в память, её инструкции лежат в последовательных ячейках именно в том порядке, в котором программист или компилятор их расположил. Поскольку СК продвигается вперёд на одну инструкцию за раз, CPU обходит эти ячейки в этом же порядке — сверху вниз, первая за последней.
Это последовательное выполнение, иногда называемое прямолинейным кодом. Это простейший возможный поток выполнения: каждая инструкция выполняется ровно один раз, в порядке, в котором она появляется в памяти, без повторов и пропусков.
Большая часть вычислений в большинстве программ последовательна: арифметика, присваивания переменных, подготовка аргументов функций. Отклонения от последовательного выполнения — ветвления и циклы — это то, что делает программы интересными, но последовательное выполнение — это базовый уровень, относительно которого измеряется всё остальное.
Почему это работает
Почему последовательность — умолчание, а не что-то другое? Последовательное умолчание непосредственно вытекает из физической конструкции схемы счётчика команд. Продвинуть регистр на фиксированное смещение требует лишь сумматора — простейшей арифметической схемы. Любое другое поведение (переход, ветвление) требует дополнительных схем и условий. Последовательное выполнение — это «ничего особенного», возникающее из простейшего железа. Это не выбор, который программист делает явно; это то, что происходит, когда никакая инструкция не переопределяет обновление СК по умолчанию.
Что нарушает прямую линию: инструкция перехода. Единственный способ отклониться от последовательного выполнения — это когда инструкция записывает другое значение в счётчик команд на шаге исполнения.
Вспомни из раздела 03 (выборка–декодирование–исполнение): шаг исполнения инструкции JUMP (переход) устанавливает СК в адрес назначения перехода вместо того, чтобы дать СК продвинуться обычным образом. Следующая выборка приходит тогда из адреса назначения, который может быть где угодно в памяти — раньше текущей инструкции, после неё или далеко от неё.
Этот один механизм — перезапись СК — лежит в основе всех непоследовательных паттернов выполнения. Ветвление — это условный переход: CPU устанавливает СК в адрес назначения только если выполнено условие, и в противном случае даёт СК продвинуться обычно. Цикл — это обратный переход: CPU возвращает СК к более раннему адресу, заставляя инструкции исполняться снова.
В оставшихся уроках раздела 07 разбирается каждый из этих паттернов. Но все они сводятся к одному вопросу: перезаписывает ли эта инструкция СК, и если да — каким адресом?
Трассировка последовательного потока через три инструкции.
Предположим, три инструкции находятся по адресам 200, 204 и 208 (каждая по 4 байта). СК начинается с 200.
Шаг 1 — Выборка из СК = 200. CPU читает инструкцию по адресу 200.
- Декодирование и исполнение: пусть это
LOAD R0, 50(загрузить значение по адресу 50 в регистр R0). После исполнения R0 хранит то, что было по адресу 50. - Обновление СК: СК = 200 + 4 = 204. Перехода не было; СК просто продвинулся.
Шаг 2 — Выборка из СК = 204. CPU читает инструкцию по адресу 204.
- Декодирование и исполнение: пусть это
ADD R0, R1(сложить R0 и R1, результат сохранить в R0). После исполнения R0 хранит сумму. - Обновление СК: СК = 204 + 4 = 208.
Шаг 3 — Выборка из СК = 208. CPU читает инструкцию по адресу 208.
- Декодирование и исполнение: пусть это
STORE R0, 60(записать значение R0 по адресу 60 в памяти). После исполнения ячейка памяти 60 хранит сумму. - Обновление СК: СК = 208 + 4 = 212.
Ни разу CPU не принимал решение о порядке. Он трижды применил правило продвижения СК по умолчанию. Поток был прямолинейным, потому что ни одна инструкция не записала другое значение в СК.
Частая ошибка
Распространённое заблуждение — считать, что порядок выполнения программы совпадает с порядком её записи в исходном файле. Для последовательного кода это в основном верно, но компилятор вправе переставить инструкции, которые не зависят друг от друга, — этот процесс называется планированием инструкций. CPU важен порядок инструкций в итоговом бинарном файле, а не в исходном тексте. CPU видит только байты в памяти; исходный код ему никогда не виден. СК указывает в бинарный файл, а не в исходный.
Инструкции находятся по адресам 40, 44, 48 и 52 (каждая 4 байта). СК начинается с 40. После исполнения инструкции по адресу 40 (не переход) чему равен СК?
Инструкции по 4 байта каждая. СК равен 200 перед исполнением JUMP на адрес 100. После исполнения JUMP чему равен СК?
Последовательно выполняются 5 инструкций по адресам 0, 4, 8, 12 и 16 (каждая 4 байта), начиная с нулевого. После выполнения всех 5 чему равен СК?
При последовательном выполнении сколько раз исполняется каждая инструкция?
Какой регистр CPU обновляет после каждой инструкции, чтобы знать, откуда брать следующую?
Что определяет поток выполнения — порядок, в котором инструкции фактически выполняются?
Поток выполнения — это последовательность инструкций, которые CPU фактически исполняет, определяемая полностью тем, какое значение хранит счётчик команд (СК) на каждом шаге выборки. Поведение по умолчанию после любой инструкции без перехода — последовательное: СК продвигается на размер инструкции, и CPU обходит инструкции в том порядке, в котором они лежат в памяти — сверху вниз, по одной. Это прямолинейное, или последовательное, выполнение. Единственный способ отклониться от этого порядка — инструкция перехода (JUMP), которая записывает другой адрес в СК на шаге исполнения, заставляя следующую выборку прийти из непоследовательного места. Каждое ветвление и каждый цикл в каждой программе построены на этом одном механизме.