Read the book: «От транзистора до трансформера. Настольная книга программиста»

Font::

Лекция 00. Введение. Зачем программисту кремний и внимание

«Если вы не можете объяснить это просто — вы не понимаете этого достаточно хорошо.» — Ричард Фейнман

Когда студент впервые приходит ко мне на курс, он обычно уже умеет писать код. Он собирал проекты, выкладывал сервисы в облако, может быть, даже поднимал собственный сервер и гордился этим вполне заслуженно. Но если спросить его, что происходит между нажатием клавиши и пикселем на экране, он почти наверняка скажет «магия». Не потому, что он ленив, а потому, что современная разработка устроена так, чтобы этот вопрос не возникал. Фреймворк прячет сеть, сборщик прячет компилятор, облако прячет железо. Каждый слой честно делает свою работу и каждый слой делает вас чуть более беспомощным, когда что-то идёт не так.

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

Почему это вообще нужно программисту, а не только инженеру железа? Потому что цена ошибки растёт вместе с уровнем абстракции. Пропущенный индекс в низкоуровневом коде стоит вам часа под санитайзером. Неверное предположение о кэше стоит дня профилирования. Ошибка в расчёте нагрузки на кластер стоит недели простоя и разговора, который вы не забудете. Чем дальше вы от железа, тем дороже обходится незнание железа и тем меньше шансов, что ошибку поймает за вас кто-то другой.

Я буду настаивать на одной мысли всю книгу. Программисту не нужно уметь проектировать процессор. Ему нужно понимать, какие договоры заключают между собой слои системы и что происходит, когда договор нарушен. Эти договоры не записаны в документации: они живут в головах инженеров и в поведении системы под нагрузкой. Наша задача — перевести их в слова, которые можно проверить экспериментом.

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

Про измерения скажу отдельно. В этой книге почти нет утверждений вида «это быстрее в десять раз» без оговорки, в каком контексте и на какой нагрузке. Инженерная правда всегда условна: быстрее для этого размера данных, для этого профиля доступа, на этом поколении железа. Как только вы перестаёте бояться этих оговорок, вы перестаёте спорить о технологиях в интернете и начинаете решать задачи. Я считаю это главным признаком взросления инженера: он перестаёт искать лучшую технологию и начинает искать подходящую.

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

И последнее, о чём я прошу перед тем, как мы начнём. Держите под рукой свой проект — любой, пусть учебный. Все примеры из этой книги можно прогнать на нём. Когда вы увидите, как теория объясняет поведение вашего собственного кода, она перестанет быть теорией. Именно в этот момент книга делает свою работу, а не тогда, когда вы дочитали последнюю страницу.

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

Лекция 01. Транзистор и логика: кусок кремния, который решает

«Компьютер — это глупейшее изобретение: он умеет только делать то, что ему велят, и делать это невероятно быстро.» — приписывается Дональду Кнуту

Начну с вопроса, который я задаю на первой лекции и на который почти никто не отвечает правильно. Что общего у процессора в вашем ноутбуке и у водопроводного крана? Студенты называют материалы, размер, цену. А общее — одно: и то и другое является клапаном, который либо пропускает поток, либо нет. Вся цифровая цивилизация построена на том, что мы научились делать такие клапаны микроскопическими, дешёвыми и переключаемыми напряжением, а не рукой. Этот клапан называется транзистор, и с него начинается всё.

Чтобы понять транзистор, не нужна квантовая механика. Достаточно одной идеи: у нас есть три вывода — исток, сток и затвор. Между истоком и стоком ток идёт только тогда, когда на затвор подано напряжение. Нет напряжения — тока нет. Есть — есть. То есть транзистор — это выключатель, которым управляет не палец, а электрический сигнал. А раз сигнал управляет выключателем, то один выключатель может управлять другим, а тот — третьим, и из этой цепочки управляемых переключателей можно собрать всё.

Почему именно два состояния? Потому что аналоговый сигнал, у которого «сколько угодно» значений, легко испортить: добавился шум — и значение уехало, и вы не отличите полезный сигнал от помехи. У сигнала с двумя уровнями есть запас прочности: пока шум не перевалил половину разницы между уровнями, мы уверенно читаем ноль или единицу. Цифровая схема — это способ получить надёжность из ненадёжных компонентов, и цена этой надёжности — отказ от всей середины между нулем и единицей. Это первое и главное решение, которое приняли инженеры, и всё остальное из него следует.

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

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

Теперь арифметика. Как из переключателей получить сложение? Очень просто, если записать сложение одного бита как таблицу: ноль плюс ноль — ноль, ноль плюс единица — единица, единица плюс единица — ноль с переносом в следующий разряд. Сумма бита — это «исключающее или», перенос — это «и». Обе функции собираются из вентилей за несколько штук. Сложите такие одноразрядные сумматоры цепочкой, передавая перенос, и вы получите сумматор на любое число бит. Всё, что делает ваш процессор с числами, в основе — эта цепочка переключателей, передающих перенос. Умножение — это сложение и сдвиги, деление — это вычитание и сдвиги. Арифметика — надстройка над переключением.

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

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

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

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

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

Что это знание меняет в вашей повседневной работе? Три вещи. Первая: когда вы слышите «это бесплатно, это же просто операция», вспоминайте, что за операцией стоят переключения и перемещения, и у того и другого есть цена. Вторая: когда выбираете алгоритм, спрашивайте не только про число операций, но и про то, сколько данных он гоняет туда-сюда, потому что перемещение часто дороже вычисления. Третья: когда ваш код медленный, не начинайте с микрооптимизаций — начните с вопроса, сколько раз вы заставляете железо переключаться и пересылать байты, которых можно было бы не пересылать.

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

Лекция 02. От вентиля к процессору: сумматоры, память, такт

«Простота — необходимое условие надёжности.» — Антуан де Сент-Экзюпери

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

Первое различие, которое нужно усвоить, — между схемами без памяти и схемами с памятью. Сумматор — схема без памяти: подали входы, подождали, пока сигнал пройдёт через вентили, получили выходы. У неё нет «до» и «после», есть только «сейчас». Она называется комбинационной. Но машина, которая выполняет программу, обязана помнить, где она находится: какую команду уже сделала и какую берёт следующей. Значит, ей нужно состояние. Состояние — это просто набор битов, которые схема хранит и обновляет. И вот тут появляется вторая идея — такт.

Зачем нужен такт? Представьте цепочку вентилей, где сигнал бежит от входа к выходу. Каждый вентиль вносит крошечную задержку. Если читать результат раньше, чем сигнал добежал до конца, вы прочитаете мусор. Можно договориться ждать «достаточно долго», но «достаточно» у каждой цепи своё, и собирать из таких цепей большую машину неудобно. Такт решает это радикально: мы вводим общий метроном и договор, что любое состояние читается и обновляется только по его удару. Между ударами сигнал успевает пройти через вентили и устаканиться. Так схема из хаотичного набора вентилей превращается в дисциплинированный автомат, где на каждом ударе метронома состояние переходит в следующее.

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

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

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

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

Цена измеряется в тактах. Если одна команда проходит через выборку, декодирование и исполнение за три такта, то программа из миллиарда команд займёт три миллиарда тактов. При частоте в гигагерц — три секунды. Кажется нормально, но мы платим за простой: на каждом такте две трети машины бездельничают. Умножьте простой на миллиарды транзисторов — и вы увидите, почему индустрия одержимо боролась за то, чтобы загружать всё одновременно. Конвейер, спекуляция, многоядерность — всё это ответы на один и тот же вопрос: как не давать транзисторам простаивать, не сжигая при этом лишние ватты.

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

Что это знание меняет в вашей работе? Первое: когда вы пишете код, представляйте, что каждая ваша строка распадается на десятки машинных команд, и каждая команда проходит один и тот же цикл. Второе: когда вам говорят «это одна операция», уточняйте, сколько тактов и сколько обращений к памяти за ней стоит, потому что «одна операция» на языке языка программирования и «одна операция» на языке железа — разные вещи. Третье: когда вы видите, что программа медленная, первым делом спросите, не заставляете ли вы машину простаивать — ждать память, гонять лишние байты, — а уже потом оптимизируйте вычисления.

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

The free sample has ended.

Age restriction:
16+
Release date on Litres:
04 October 2026
Writing date:
2026
Volume:
80 p. 1 illustration
Copyright Holder::
Автор
Download format:

Similar books