Раздел 2 · Программирование / CS

Машина под уроком: процессор, память, кэш и ОС

junior~40 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Машина под уроком: процессор, память, кэш и ОС

Два цикла делают одну и ту же работу: складывают все элементы большой матрицы. Один идёт по строкам, другой по столбцам. Количество сложений одинаковое, Big-O одинаковая, а время отличается в несколько раз. Если твоя модель программы заканчивается на “процессор выполняет операции”, объяснить это нечем.

// одинаковая работа, разное время
for (let i = 0; i < n; i++)
  for (let j = 0; j < n; j++) sum += m[i][j]; // быстро

for (let j = 0; j < n; j++)
  for (let i = 0; i < n; i++) sum += m[i][j]; // медленно, в разы

В прошлом уроке мы научились читать биты. Сегодня посмотрим, где они живут: процессор и его слово, адреса, сегменты процесса, стек и куча, кэш и его линии, и сверху роль операционной системы. Это фундамент, на который опираются event loop, воркеры Node и буферный пул СУБД.

Карта урока

  1. Процессор: регистры, такты, разрядность, машинное слово.
  2. Память как массив байтов: адреса и сегменты процесса.
  3. Стек вызовов и куча: два способа жить в памяти.
  4. Кэш: L1, L2, L3, cache line и локальность. Здесь и живёт ответ про матрицу.
  5. ОС: процессы, потоки, планировщик, вытесняющая многозадачность.
  6. Управление памятью: malloc и free против сборщика мусора.

Раздел 1 · Процессор: регистры, такты и слово

Идея словами

Ядро процессора умеет на удивление мало: прочитать число из памяти в регистр, сложить два регистра, записать регистр обратно в память, сравнить и прыгнуть на другую инструкцию. Всё. Любой твой array.map после всех слоёв компиляции превращается в перекладывание чисел между памятью и регистрами.

Темп задаёт тактовая частота: 3 ГГц значит три миллиарда тактов в секунду, простая инструкция занимает порядка одного такта. Запомни этот масштаб: такт это треть наносекунды, и дальше мы будем мерить им стоимость походов в память.

Разрядность это ширина регистра и адреса. На 64-битной машине указатель занимает 8 байт, и это число ты уже видел в таблице размеров прошлого урока. 32-битные системы с их потолком адресов в 4 ГиБ (2^32 байта) ещё живы во встраиваемом мире и в WebAssembly: линейная память WASM в базовой версии адресуется 32 битами, подробности в уроке про WASM.

Что взять с собой

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

Раздел 2 · Память: массив байтов и сегменты процесса

Идея словами

Вся оперативная память для программы выглядит как один гигантский массив байтов, а адрес это просто индекс в нём. Каждый процесс при этом видит своё адресное пространство: ОС и железо подставляют между виртуальными адресами программы и физическими ячейками таблицу трансляции, поэтому два процесса не видят память друг друга. Этой изоляцией мы займёмся в разделе 5.

Внутри адресное пространство процесса нарезано на сегменты:

высокие адреса
+----------------+
|  stack         |  стек вызовов, растёт вниз
|      ↓         |
|                |
|      ↑         |
|  heap          |  куча, растёт вверх
+----------------+
|  bss / data    |  глобальные и статические данные
+----------------+
|  text          |  машинный код программы
+----------------+
низкие адреса

text это сам код, data и bss это глобальные переменные, а два по-настоящему интересных сегмента, стек и куча, растут навстречу друг другу и заслуживают отдельного раздела. Если захочешь увидеть эту схему глазами системного языка, она же разобрана в Go-треке и в разговоре про указатели и память Go.

Что взять с собой

Память это массив байтов, адрес это индекс, каждый процесс видит своё виртуальное пространство, нарезанное на text, data, heap и stack.

Раздел 3 · Стек вызовов и куча

Сцена

Ты видел оба сегмента в деле тысячу раз. RangeError: Maximum call stack size exceeded это перелив стека. “Вкладка ест два гигабайта” это куча. Осталось связать их в одну картинку.

Идея словами

Стек вызовов работает как стопка тарелок. Вызов функции кладёт сверху кадр: аргументы, локальные переменные, адрес возврата. return снимает кадр. Аллокация на стеке это сдвиг одного указателя, поэтому она практически бесплатна, но за дешевизну платим двумя ограничениями: размер стека мал и фиксирован, а время жизни данных жёстко привязано к вызову. Глубокая рекурсия без выхода переполняет стек, отсюда RangeError, отсюда же трюк с трамплином из урока про IO-монаду.

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

Как это ложится на JS. Движок хранит на стеке кадры вызовов и короткие примитивы внутри них, а объекты, массивы, функции и строки живут в куче, на стеке лежат ссылки на них. Отсюда два знакомых факта: копирование объекта копирует ссылку, а замыкание из урока про scope может держать окружение сколь угодно долго, потому что движок вынес это окружение в кучу. Точные детали V8 сложнее (escape analysis, inlining), но модель “кадры на стеке, долгожители в куче” верна и достаточна.

Что взять с собой

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

дальше после входа

Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.

Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.