2.7 Что делает процессор

Ранее мы изучили, как компьютер работает с разными типами информации. Теперь углубимся в процессор — самый главный элемент, который проводит расчёты.

Наш разговор пойдёт так:

  • Познакомимся с понятием «разрядность процессора» и обсудим регистры.
  • Рассмотрим процесс выполнения кода на старой архитектуре 16-битного процессора.
  • А затем сравним, как этот процесс изменился в современной архитектуре.

Приступим!

Разрядность процессора и регистры

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

Процессоры различаются по архитектуре. Давайте дадим определение этому понятию.

Архитектура процессора — это набор правил, методов и моделей, которые определяют, как процессор выполняет команды и управляет данными.

Вот примеры самых популярных архитектур:

  • x86 — в компьютерах.
  • ARM — в смартфонах и планшетах.
  • AVR — в микроконтроллерах встроенных систем (автомобили, телевизоры и тому подобное).

Исходя из определения архитектуры, если мы запустим программу, разработанную под архитектуру x86, на смартфоне, то она не заработает. Потому что у архитектуры ARM свои правила и методы для процессора.

Это как приделать колесо от автомобиля к самокату: и то, и другое колесо, но у них разные крепления и диаметр арок.

Впрочем, есть исключения

Например, можно взять фреймворк Flutter, написанный на языке программирования Dart. Этот фреймворк способен компилировать код под требуемую архитектуру процессора.

Это даёт разработчикам гибкость: можно написать код один раз и на выходе получить версию, оптимизированную под веб, мобильные устройства и компьютеры.

Так как в хендбуке мы фокусируемся больше на компьютерах, в этом параграфе будем говорить об архитектуре для компьютеров — x86.

Своё название она берёт от модели первого 16-битного процессора — Intel 8086, который появился в 1978 году. Забавно, что номер 8086 никак не привязан к году выпуска, а был просто порядковым номером модели после 8085.

В дальнейшем Intel сохранили привязку к x86 для узнаваемости в своих моделях: 80186, 80286, 80386, 80486. Это был своеобразный маркетинговый ход. Позднее, когда появились 32- и 64-битные процессоры, их архитектуру стали называть x86/x86-32, x86-64. Если объединить сказанное выше вместе, то получится следующая наглядная табличка:

1.6.1

Сейчас самое время поговорить о битности, её ещё называют разрядность процессора. Если простыми словами, это число определяет, кусок информации какого объёма процессор способен «переварить» за раз.

У 16-битных процессоров разрядность составляет 2 байта (помним, что 1 байт — это 8 бит). У 32-битных — 4 байта. У 64-битных — 8 байт.

Также от разрядности зависит, к какому количеству уникальных адресов памяти процессор может обратиться — от 0 до 2Разрядность. Если собрать всё.

У процессора есть свои два типа памяти, которые он активно использует: регистры и кэш-память.

  • Регистры — маленькие и самые быстрые ячейки памяти, расположенные на самом процессоре. В них загружаются числа при вычислениях, с которыми процессор работает прямо сейчас.
  • Кэш-память — это очень быстрая, но ограниченная по объёму память, встроенная в сам CPU. Она служит «прослойкой» между медленной оперативной памятью (RAM) и очень быстрыми ядрами процессора. Позже в параграфе рассмотрим реализацию этой концепции на примере архитектуры современного процессора, так как кэш-память появилась не сразу.

Давайте сфокусируемся на регистрах.

Размеры регистров зависят от разрядности. У 16-битного процессора 8 регистров общего назначения, состоящих из 16 бит. Они называются так: AX, BX, CX, DX, SI, DI, BP, SP. И каждый способен хранить 2 байта.

Маленький нюанс

Каждый из них отвечает за свою задачу, но рассказ об этом выходит далеко за пределы нашего параграфа.

Если вам интересно узнать глубже, то почитать можно тут.

Каждый регистр состоит из двух 8-битных частей: старший байт AH + младший байт AL. Таким образом, взаимодействовать можно либо со всем регистром целиком, либо с его частью.

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

Это значит, что, когда появились 32-битные процессоры, они могли работать с 16-битными регистрами. Для этого были созданы расширенные регистры EAX, которые содержали дополнительно 16 бит, а другие 16 бит уходили под AX. И к AX всё так же можно обращаться напрямую. Получаем матрёшку, в которой регистры предыдущих поколений вкладываются в новые.

Аналогично и для 64-битного процессора появились ещё расширенные регистры в 64 бита RAX, в рамках которых можно обратиться и к EAX, и к AX, и к AH.

1.6.2.webp

Также существуют регистры специального назначения, которые нужны не для хранения обычных чисел, а для управления самим процессором, памятью и выполнением программ.

Глубоко погружаться в их устройство не будем, расскажем лишь, зачем применяются:

  • Регистры для стека — управляют вызовами функций и локальными переменными (BP, SP).
  • Флаговые регистры — фиксируют результат операций (FLAGS).
  • Указатель команд — указывает на следующую инструкцию (IP/EIP/RIP).
  • Сегментные регистры — задают области памяти (CS, DS, SS…). В современных 64-разрядных процессорах сегментация почти не используется.

1.6.3

Регистры — это самая быстрая для доступа память процессора. Поэтому данные для большинства небольших промежуточных вычислений он держит в них, в регистрах.

Теперь вы знаете, где процессор хранит данные для обработки текущих операций и в каком виде. Чтобы сложилась полная картина, осталось понять, как эти данные в процессор попадают и как процессор обрабатывает команды.

Как процессор считывает данные из памяти

Для погружения рассмотрим, как обрабатывается код на старой 16-битной архитектуре процессора, которую рассматривали в параграфе 2.4. А затем посмотрим на отличия в современной архитектуре. То есть сначала изучим базовые принципы, а потом увидим, как они модифицировались.

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

1.3.1

Представим, что вы нажимаете кнопку «Включить компьютер» на 16-битном процессоре. Давайте рассмотрим, что произойдёт после.

В первую очередь процессор будет обрабатывать инструкции, которые поступят ему из ПЗУ (ROM, BIOS), так как там хранятся настройки для запуска операционной системы (ОС). Пока не будем углубляться, что это такое, — держим в уме, что это сущность, которая позволяет пользователю работать с компьютером. Подробнее об операционной системе поговорим в следующей главе.

Продолжим. Память ПЗУ (как и RAM, как и жёстких дисков) состоит из набора адресов, а в каждом адресе лежит по 1 байту информации. Это байтовая адресация, которая сохраняется и в работе современных компьютеров.

1.6.16

Допустим, мы хотим обратиться к адресу 1010 1010 1010 0010 и получить данные из этой ячейки у ПЗУ. Чтобы это сделать, процессор обращается к системной шине, которая реализует взаимодействие процессора с другими элементами.

Количество данных, которое мы сможем получить за раз, зависит от разрядности шины.

С 16-битным процессором чаще всего использовались 8-битные шины данных. Так что для примера возьмём такую разрядность. Значит, за раз процессор сможет передать 1 байт.

Сама системная шина состоит из нескольких блоков линий, которые задействуются в определённом порядке: адресная шина, шина управления, шина данных.

Первым делом задействуется адресная шина (Address Bus), по ней процессор передаёт адрес ячейки памяти.

Шина состоит из линий. На линиях A0 — A15 выставляется адрес. В нашем случае — 0100 0101 0101 0101 (перевернутый адрес, так как A0 — это младший бит, а A15 — старший бит). Этот адрес передаётся в ПЗУ (ROM).

1.6.5

Вторым шагом настраивается, что именно нужно сделать. На шине управления (Control Bus) процессор выставляет набор из двух сигналов: RD (read) и WR (write).

Если сигнал равен 0, значит, этот сигнал активен, если 1 — неактивен. Поскольку мы работаем с ПЗУ, а в него данные невозможно записать по определению, сигнал WR будет неактивен, а сигнал RD активен, то есть WR = 1 и RD = 0. Таким образом, по шине управления передалось, что мы хотим считать информацию.

В свою очередь у ПЗУ выставляются ответные сигналы: CE (Chip Enable) = 0 — активируется, чтобы выбрать нужный чип памяти. OE (Output Enable) = 0 — разрешает выход данных из ПЗУ на шину.

Занимательный факт

Мы часто подсвечивали, что единица обозначает «что-то есть» или «работает», а ноль — «ничего нет» или «не работает».

Как вы заметили, для выставления сигналов на шине управления всё точно наоборот. Это связано с тем, что в 1970-х годах, когда элементы только разрабатывались, чисто физически было проще активировать работу схемы, выдав на транзисторы сигнал 0, а не 1.

Для такой ситуации появилось понятие «активный ноль», когда сигнал равен 0, а система работает.

Получился обмен сигналами управления и подготовка к этапу считывания информации.

1.6.6

И последним шагом задействуется шина данных (Data Bus). Один байт информации состоит из 8 ячеек с 0 или 1. Они выгружаются на линии D0 — D7 и передаются в процессор. Если бы шина была 16-битная, то считывались бы два байта, то есть одновременно из заданного адреса и соседнего, а информация ложилась бы на линии D0 — D15.

Например, в ячейке с нашим адресом хранится число 01101000 (104). Как и в случае с адресом на D0 пойдёт младший бит, а на D7 старший. То есть на линиях будет число 00010110.

1.6.7

После передачи данных сигнал RD отпускается, и шина освобождается до следующего цикла.

Эта циклическая последовательность действий называется циклом чтения из памяти (Read Bus Cycle). Если схематически изобразить, что происходит на линиях в каждый момент, то выглядело бы это так:

1.6.8

Пара слов о том, как развивались шины

От количества линий, передающих адрес, зависит, с памятью какого объёма может работать шина. В нашем примере у адресной шины 16 линий, значит, максимальный размер ОЗУ для памяти — 64 КБ ().

С ростом объёма ОЗУ потребовалось больше адресных линий. Например, чтобы работать с 1 МБ оперативной памяти, требовалось 20 линий (). Соответственно менялась их нумерация: A0 — A15 → A0 — A19.

Но общее количество линий могло и уменьшаться! Существовали мультиплексированные шины — например, у процессоров Intel 8086/8088. В них по одной и той же линии могли идти и адрес, и данные.

Это удешевляло стоимость процессора. Соответственно менялась и нумерация: вместо использования линий A0 — A15, достаточно было A0 — A7 и D0 — D7.

Современные шины не мультиплексированные. Им важна скорость передачи данных, поэтому процессы передачи адреса и данных идут параллельно, что невозможно в мультиплексированной модели.

Таким образом реализована передача данных на системной шине. Аналогично процессор может запросить считывание или запись данных и в ОЗУ — с единственным отличием: в операционную память можно записывать данные, то есть сигнал WR может быть 0.

1.6.9

Как видите, считывание процессором данных происходит через шину, что последовательно активирует три её составляющие: адресную шину, шину управления и шину данных.

Процессор может обратиться к любому однобайтовому элементу памяти по его адресу, считать его или в случае ОЗУ туда что-то записать.

Как процессор считывает и запускает код

Теперь рассмотрим, как код программы превращается в 8-битные кусочки информации в памяти, чтобы процессор мог её считать и обработать. Как упоминали ранее, в компьютерах побайтовая адресация — и вся память хранится по байтам.

Сейчас программы пишутся на большом количестве различных языков: Python, C++, Ruby и так далее. Но каждый из них должен обрабатываться процессором. Для этого существует ассемблер и одноимённый язык. Зафиксируем различия:

  • Язык ассемблера — низкоуровневый язык программирования, на котором команды почти один в один соответствуют машинным инструкциям.
  • Ассемблер — это программа-транслятор, которая переводит код, написанный человеком на языке ассемблера, в машинный код (который точно поймёт процессор).

Когда вы запускаете код на C++, компилятор C++ приводит код к языку ассемблера, а затем ассемблер переводит в машинный код. Таким образом ваша написанная программа превращается в язык, понятный для процессора.

Занимательный факт

У каждого процессора есть свой ассемблер и язык ассемблера. Получается, что разных языков ассемблеров столько, сколько есть разных архитектур процессоров. Самые популярные из них: x86, x86-64, ARM, AArch64, MIPS, RISC-V, PowerPC, SPARC, VAX, Alpha, Itanium, Z80, 6502.

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

1.6.10

Рассмотрим подробнее, что тут происходит.

Наша простая программа:

  • Присваивает переменной a значение 10.
  • Присваивает переменной b значение 5.
  • Присваивает переменной c сумму значений переменных a и b.
  • Если c больше 20, то присваивает переменной c значение 100.
  • Присваивает переменной c сумму значений переменной с и 5.
  • Возвращает значение переменной c.

Этот код переводится на язык ассемблера:

  • Положить значение 10 в регистр AX.
  • Положить значение 5 в регистр BX.
  • Добавить значение из регистра BX в регистр AX.
  • Сравнить значение в регистре AX со значением 20.
  • Если значение меньше или равно, то прыгнуть на метку L1.
  • Положить значение 100 в регистр AX.
  • Метка L1.
  • Добавить значение 5 к числу в регистре AX.
  • Прервать программу.

В итоге в регистре AX лежит необходимое нам значение. Программа в ассемблере делает то же самое, но команды стали более прямолинейными.

Далее ассемблер переводит код с языка ассемблера в машинный код.

В машинном коде каждая команда переводится в набор ноликов и единичек. Например, mov ax, 5 превратилось в B8 0A 00 (в шестнадцатеричном виде для удобства, чтобы не писать 24 цифры).

Важно: каждая строчка кода на языке ассемблера — это инструкция, которую процессор потом будет реализовывать. То, что ассемблер превратил в непонятный для человека набор из чисел B8 0A 00, будет максимально понятно процессору — что именно от него хотят.

И когда в будущем он начнёт считывать данные из памяти, то, получив значение инструкции B8, он поймёт, что это команда mov, которая записывает в регистр ax значение. Надо считать ещё 2 байта: там лежит значение, которое нужно положить.

На рисунке выше мы отобразили машинный код построчно, чтобы нагляднее показать вам соответствие между линиями. В реальности машинный код идёт подряд и лежит побайтово в двоичном виде.

Занимательный факт

Команды, которые понимает процессор, регистры, адресация к памяти возникли далеко не сразу. На ранних процессорах, в 1950–60-х годах, начали появляться небольшие наборы команд у процессоров.

А в 1964 году IBM публично ввела термин ISA (Instruction Set Architecture — архитектура набора инструкций процессора) и выпустила первый стандарт, которого придерживались процессоры.

Это позволило решить проблему, что запущенная на одной машине программа не работала на другой, потому что процессоры понимали разные наборы команд.

С тех пор ISA для разных архитектур процессоров активно развивалась, увеличивая количество команд и усложняя их. Перечень всех команд, которые может воспринимать процессор на данный момент, можно посмотреть тут.

Как итог, машинный код программы после запуска записывается в оперативную память, а дальше процессор начинает её считывать.

Давайте для примера допустим, что весь машинный код записался в ячейки памяти — начиная от 1000h. Здесь h — сокращение от hexadecimal — указывает, что число шестнадцатеричное.

Тогда машинный код расположится там следующим образом:

1.6.16

Этот код и будет обрабатывать процессор. Далее происходит обработка кода повторяющимся циклом, получившим название Fetch — Decode — Execute — Store. Шаги обработки:

  1. Выборка инструкции из памяти (Fetch).
  2. Декодирование и выборка операндов (Decode).
  3. Выполнение (Execute).
  4. Запись результата (Store).

Кстати, а помните, как выглядела фоннеймановская архитектура устройства компьютера, которую разбирали в параграфе 2.4? Посмотрите на схему ниже.

1.3

На самом деле вы сейчас ныряете поглубже в каждый элемент центрального процессора: в устройство управления и в арифметико-логическое устройство. Ведь именно там происходит обработка кода и обозначенный выше цикл Fetch — Decode — Execute — Store:

1.6.11

Давайте рассмотрим эти шаги подробнее.

Шаг 1. Выборка инструкции из памяти (Fetch)

Процессор извлекает следующую инструкцию из ОЗУ. Адрес нужной команды хранится в счётчике команд в регистре IP (Instruction Pointer — указатель команд). Перед началом запуска нашей программы IP = 1000h, что указывает на первую ячейку памяти, где хранится программа.

При старте процессор запрашивает содержимое по адресу 1000h и считывает байт инструкции с помощью шины, как мы рассматривали ранее в этом параграфе.

1.6.12

Инструкция кладётся в IR (Instruction Register — регистр команд). Таким образом, в IR лежит B8. На этом этапе процессор не знает, что это за инструкция и как её обрабатывать. Поэтому передаём её декодеру на следующий шаг, чтобы нам пояснили. Переходим к декодированию считанной инструкции.

Шаг 2. Декодирование и выборка операндов (Decode)

Поступивший машинный код команды расшифровывается декодером, встроенным в блок управления. Декодер определяет, что должна делать команда (например, сложение, чтение из памяти, переход) и с какими данными.

Инструкция в машинном коде состоит из двух частей — опкода и операнда.

  • Опкод (opcode) — набор битов, в котором лежит информация о том, какую машинную инструкцию реализовывать и какой режим адресации использовать (например, поместить в определённый регистр).
  • Операнды (operands) — над чем выполняется операция (регистры, константы, адреса памяти).

1.6.24

В нашем случае декодер смотрит опкод в регистре IR, то есть B8. Декодер понимает, что это MOV r16, imm16, то есть команда MOV, в качестве назначения регистр AX, а дальше идут два байта — непосредственное (immediate) значение, которое загружается в регистр. Если интересно, перечень всех опкодов и их трактовок можно посмотреть тут.

У нас пока нет считанных значений для работы функций (операндов). IP указывает на ячейку 1001h. Значит, считываем значения с неё. И так как нам нужно два байта в операнд, то считываем ещё и 1002h. Вместе они образуют значение, которое надо будет переместить в AX.

Теперь мы знаем полностью и инструкцию, и операнды, к которым её нужно применить. Можем переходить к шагу выполнения.

1.6.13

Шаг 3. Выполнение (Execute)

После того как инструкция загружена и расшифрована, наступает самый главный этап — её выполнение. На этом этапе процессор делает то, что предписано инструкцией.

  1. Расчёты. Если надо что-то посчитать, то данные идут в арифметико-логическое устройство (АЛУ). Здесь выполняются операции сложения, вычитания, умножения, деления, логические операции, а также сравнение и инкременты/декременты.
  2. Перемещение данных (MOV). Данные перемещаются в нужное место: конкретный регистр или адресованную память.
  3. Работа со стеком. Временное сохранение данных или адресов в памяти при помощи команды PUSH и их последующее извлечение обратно с помощью POP.
  4. Переходы и вызовы. Проверяются флаги, изменяются счётчики инструкций (IP).
  5. Ввод/вывод. Данные идут напрямую на устройства ввода-вывода.

В нашем случае хранимое значение 10, равное перевёрнутому операнду 0A 00, просто помещается в регистр AX.

1.6.14

Почему перевёрнутому?

Потому что в большинстве архитектур, начиная с x86 и до современных, порядок хранения данных перевёрнут: младший байт числа хранится по меньшему адресу, а старший — по следующему (англ. Little-endian).

Таким образом наше значение равно 000A, то есть 10.

Так как никаких арифметических расчётов не происходит, АЛУ здесь не используется.

Шаг 4. Запись результата (Store)

Если после реализации расчётов из АЛУ инструкция меняет память или результат, то происходит запись результата.
А дальше переходим к следующей инструкции. IP указывает на 1003h, цикл запускается заново.

1.6.15

Программа в виде машинного кода последовательно обрабатывается в рамках цикла Fetch — Decode — Execute — Store. Инструкции обрабатываются последовательно до момента, пока не закончится программа. В один момент может обрабатываться только одна инструкция.

Мы рассмотрели реализацию полного цикла одной инструкции. А интересно, сколько времени это заняло? Чтобы примерно понять, насколько быстро обсчитается наша конкретная инструкция и программа в целом, познакомимся с термином такт процессора.

Такт процессора (clock cycle) — это минимальная единица времени в процессоре, которая задаётся сигналом тактового генератора (clock). Все блоки процессора синхронизируются по этим импульсам.

Синхронизируются — значит все переключения электронных схем (например, транзисторов) происходят в один момент, когда приходит тактовый сигнал. Так каждая часть схемы знает, когда ей можно менять своё состояние.

1.6.17

За один такт процессор успевает сделать одно элементарное действие. Вот примеры таких действий:

  1. Выставить адрес.
  2. Подготовиться к обмену.
  3. Передать данные по внутренней шине.
  4. Считать байт из памяти.
  5. Обновить регистр.
  6. Выполнить часть операций в АЛУ.

Количество тактов напрямую связано с частотой процессора. Например, если у процессора 8086 была частота 5 МГц, то это означало, что он способен выполнять 5 млн тактов в секунду. То есть порядка 5 млн элементарных действий в секунду.

Если посмотреть, сколько тактов обрабатывается первая инструкция, то получится порядка 16.

  • Нужно считать 3 байта информации, это 3 запроса, каждый запрос обрабатывается 4 такта (как раз выше и разбирали цикл чтения памяти, который состоит из четырёх шагов, каждый из которых выполняется за такт).
  • Ещё 4 такта уйдёт на реализацию инструкции. Для каждого процессора существует перечень таймингов, необходимых для реализации той или иной инструкции. Для 8086 можно посмотреть тут (ресурс недоступен на территории РФ), для широкого круга моделей процессоров — тут.

Получается, три раза считываем данные для инструкции — это шаг Fetch, суммарно 12 тактов. Шаг Decode в ранних процессорах почти ничего не стоил, условно 0 тактов. Шаг Execute занимает 4 такта на реализацию инструкции. Схематически это выглядит так:

1.6.18

Осталось только умножить количество тактов на такт процессора — и получим время исполнения одной конкретной инструкции mov ax, 5. Для процессора с частотой 5 МГц, такт процессора равен 200 нс. Время исполнения: 16 * 200 нс = 3,2 мкс (микросекунды).

Таким образом, вы прошли путь от формирования инструкции, сохранения её в памяти, считывания и обработки. Теперь вы знаете, как запускается код на архитектуре 16-битных процессоров по типу 8086.

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

  • Чтобы получить данные, процессор обращается по адресу и с помощью шины считывает всю необходимую информацию.
  • Машинный код программы, который считывается процессором, появляется последовательно. Сначала компилятор трансформирует код с языка высокого уровня в ассемблер-код. А затем ассемблер переводит его в машинный.
  • Каждая инструкция программы обрабатывается по циклу Fetch — Decode — Execute — Store.

Так работал процессор на архитектуре x86 почти 50 лет назад. Разумеется, за это время она обросла разными улучшениями и новыми идеями. Давайте коротко их рассмотрим, это даст нам представление о работе современных процессоров.

Отличия в обработке кода на современной архитектуре

Важно: мы не будем углубляться в детали, иначе параграф быстро превратится в книгу. Вместо этого в общих чертах рассмотрим основные изменения.

Сосредоточимся на следующих улучшениях:

  • Расширение разрядности.
  • Добавление параллельности.
  • Увеличение скорости доступа к памяти.
  • Появление аппаратного предсказания переходов.
  • Усовершенствование АЛУ.
Расширение разрядности

Суть: современные CPU перешли на 64-битную разрядность регистров и адресов (x86-64/AMD64), расширили число регистров общего назначения (с 8 до 16 в x86-64) и оперируют более широкими данными за такт. Это фундаментально увеличило адресное пространство и пропускную способность обработки данных.

Раньше 8-битная шина могла за один раз выгрузить только один байт информации. А теперь 64-битная шина может выгрузить сразу 8 байт, что уменьшает количество тактов для этапов Fetch и Decode.

Также в работе современных процессоров не встретишь взаимодействие с одной большой системной шиной. С каждым элементом теперь есть своя шина, которая ускоряет взаимодействие и не зависит от других элементов (подробнее упоминали об этом в параграфе про архитектуру компьютера).

Добавление параллельности

Суть: вместо последовательной модели 16-битных систем команды проходят сегодня через глубокий конвейер (pipeline).

Раньше инструкции выполнялись последовательно: следующая не начиналась, пока не обработается предыдущая:

1.6.19

В современных компьютерах выполнение больше похоже на параллельные конвейеры. Визуально это можно изобразить так:

1.6.20

Также процессоры стали многоядерными, что позволяет запускать конвейерную обработку одновременно на нескольких ядрах.

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

Суть: процессоры используют иерархию кэшей (L1/L2/L3) для инструкций и данных, чтобы подавать нужные байты за считанные такты.

Работа с памятью стремится к тому, чтобы найти необходимые данные как можно быстрее. Для этого в процессор встраивают промежуточные кэши, которые содержат информацию о последних операциях и текущих переменных.

В ранних процессорах кэшей не было, данные брались из ОЗУ или регистров. Со временем тактовая частота процессора начала по скорости обгонять время, затрачиваемое на соединение с ОЗУ, что способствовало развитию кэшей, к которым процессор мог бы обращаться максимально быстро на новых скоростях.

1.6.21

Пара слов о кэшах

Самый быстрый источник данных — это регистры, к ним процессор может обратиться всего за 1 такт.

Следующим по скорости идёт L1 Cache. У него объём памяти 16–128 КБ на ядро, скорость доступа — 1–3 такта.

В L1 хранится то, что используется прямо сейчас. Он делится на L1 Data Cache (D-Cache) и L1 Instruction Cache (I-Cache).

  • В D-Cache хранятся часто используемые данные, например переменные или элементы массива.
  • В I-Cache хранятся инструкции программы, чтобы не обращаться каждый раз к RAM.
    Коротко напомним, что L1 Cache — это элемент, который появился как раз в гарвардской архитектуре и реализует идею разделения на память для данных и память для инструкций:

1.3

Кэш следующего уровня — L2 Cache. Он служит подстраховкой для L1 Cache и хранит больший объём данных, с которыми процессор недавно взаимодействовал. Он не делится на данные и инструкции, хранит всё вместе. Его объём в несколько раз больше, чем у L1: 256 КБ — 2 МБ на ядро. А скорость доступа — 5–15 тактов.

Последний уровень кэша — L3 Cache — самый большой и самый медленный среди рассмотренных. Но он доступен одновременно для всех ядер. В нём хранится расширенный список инструкций и данных, которые не поместились в L1, L2, но активно используются разными ядрами.

Его объём составляет от 2 до 64 МБ, скорость доступа — 30–60 тактов.

Ниже — схема расположения кэшей L1, L2 и L3 у процессора:

1.6.22

Для сравнения: обращение к RAM занимает сотни тактов (200–300), а к самой быстрой внешней памяти SSD (NVMe) — порядка сотни тысяч тактов.

Появление аппаратного предсказания переходов

Суть: процессор поддерживает непрерывную загрузку конвейера и резко снижает простои при ветвлениях.

Представим, что в процессор прилетает инструкция, которая содержит условие if, for, while, jmp. Рассмотрим код ниже:

1cmp eax, ebx
2je L1
3mov ecx, 5

В конвейерном режиме работы все инструкции считываются подряд и запуска je L1, процессору не хватает данных для принятия решений: ему нужно дождаться обработки инструкции cmp eax, ebx. Из-за этого линия стоит и начинаются простои тактов.

Что за команда je?

Расшифровывается как Jump if Equal, то есть «перейти к метке L1, если в предыдущей операции было равенство».

А что, если не ждать, пока выполнится инструкция cmp eax, ebx, а взять и предсказать, что значение, скорее всего, будет истинным, и начать обрабатывать конвейер дальше? Конечно, будут случаи, когда мы ошиблись. Тогда система делает откат — и получаем штраф в дополнительные 10–20 тактов. Но если мы угадали, то сэкономим порядка 10 тактов.

И во сколько это позволяет сократить простой?

Вот как отрабатывает модель без предсказаний:

  • В среднем инструкции с ветвлением ждут порядка 10 тактов, пока получат нужное значение. В этот момент конвейер простаивает.
  • Инструкций с ветвлением — 15–20% во всём коде. Далее для простоты возьмём минимальную планку — 15%.
  • Получается, если не делать предсказание, то на 100 инструкций 15 из них будут с ветвлением и общий простой получится примерно 15 × 10 = 150 тактов.

С предсказаниями модель будет следующая:

  • Если угадываем, куда пойдёт ветвление, то простоя не возникает.
  • В случае ошибки получаем простой в 15–20 тактов. Далее возьмём максимальную планку — 20 тактов.
  • На современных процессорах процент угадывания, по какой ветке пойдёт дальше код, составляет более 95% (а в некоторых случаях и 99%). То есть в 5% случаев будет возникать ошибка. Из 15 инструкций с ветвлением 15 × 0,05 = 0,75 будут с ошибкой.
    Общий простой будет 0,75 × 20 = 15 тактов.

Как показали математически, простой сократился в 10 раз.

Усовершенствование АЛУ

Суть: в современных процессорах АЛУ обрабатывает более сложные операции и может параллельно делать расчёты для нескольких инструкций.

В процессоре 8086 АЛУ было относительно простым: оно работало строго с 16-битными числами и выполняло базовые операции: сложение, вычитание, побитовые логические функции и сдвиги.

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

У современных процессоров появилось несколько улучшений:

  • Несколько инструкций могут обрабатываться параллельно за счёт наличия нескольких АЛУ.
  • Для сложных вычислений в виде умножения или деления появились отдельно заточенные под это модули.
  • Стала возможна обработка вычислений для чисел с плавающей точкой.
  • Появились векторные блоки (SIMD-блоки, Vector ALU) — специальные исполнительные устройства, которые могут выполнять одну и ту же операцию сразу со множеством данных.
SIMD-блоки

Принцип их работы такой: одна инструкция управляет обработкой нескольких элементов одновременно.

Например, инструкция vaddps складывает 16 пар чисел с плавающей точкой (32-битных) за один такт, в то время как обычное АЛУ сделало бы только одно.

Схематически работу современного процессора в рамках цикла Execute с учётом улучшений можно изобразить так:

1.6.23


На этом всё! Было сложно, но вы справились!

Давайте коротко подведём итоги параграфа:

  1. У процессора есть регистры и кэш. Всё это — память с очень быстрым доступом, за что приходится расплачиваться малым объёмом.
  2. Количество информации, которое способен обработать процессор за один такт, называется разрядность. А количество тактов определяется частотой процессора.
  3. Процессор считывает инструкции и данные из памяти, проходя по циклу чтения памяти (Read Bus Cycle). И заносит данные в регистры.
  4. Чтобы выполнить код, процессору требуется ассемблер, который переведёт код в нули и единицы. А уже нули и единицы превращаются в инструкции с помощью цикла Fetch — Decode — Execute — Store, который применяется до сих пор.

Надеемся, после параграфа вы в общих чертах поняли, как процессор обрабатывает наши любые действия, сколько операций это требует и как молниеносно они вычисляются.

Но мы копнули только верхушку айсберга! Если вам интересно углубиться в суть, советуем пройти по ссылкам, которые есть в этом параграфе.

А в следующем параграфе мы коротко подведём итоги всей главы.

Чтобы добавить в заметки выделенный текст, нажмите Ctrl + E
Предыдущий параграф2.6. Как компьютер работает c информацией
Следующий параграф2.8. Что вы узнали из этой главы