Экспресс-курс · No. 24
Работа над производительностью идёт не так одинаково каждый раз: кто-то гадает, что медленно, оптимизирует не то и добавляет сложность впустую. Дисциплина обратна — измерь, чтобы найти реальное узкое место, почини то одно, потом измерь снова. Выучи, что на деле делает софт медленным, и слова для этого, и «сделай быстрее» станет методом, а не догадкой.
Только суть · Один образ на идею · Выучи слова
Первое правило производительности — то, что нарушают все: не доверяй своему чутью о том, что медленно. Почти вся впустую потраченная оптимизация идёт от пропуска этого единственного шага.
Твоя интуиция о медленности обычно неверна
Врач, что назначает операцию по догадке, ни разу не сделав анализ, — уверенный, быстрый и слишком часто оперирующий совсем не то.
Разработчики знаменито плохи в угадывании, куда уходит время. Часть, в чьей медленности ты уверен, часто в порядке; настоящий виновник где-то, где ты и не подозревал, — крошечная функция, вызванная миллион раз, скрытый вызов базы. Действовать по чутью значит оптимизировать не то, добавить сложность, а программа остаётся медленной. Первый ход — никогда не «почини», а узнай, куда на деле уходит время.
Профилируй, чтобы найти реальную стоимость
Детализированный счёт, что показывает ровно, куда ушли деньги, строка за строкой, — так ты перестаёшь гадать и видишь, что один платёж, а не те, что ты предполагал, съел бюджет.
Профайлер (profiler) — это инструмент, что меряет, где твоя программа на деле проводит время, функция за функцией. Он превращает «думаю, это медленно» в «эта одна функция — 80% времени выполнения». С этим ты чинишь то, что важно, вместо того, что вообразил. Профайлер ли это, тайминговые логи или твои метрики наблюдаемости, принцип держится: дай измерению, а не интуиции, указать тебе на проблему.
Измерь, почини, измерь снова
Учёный меняет одну переменную, перезапускает эксперимент и проверяет результат — никогда не предполагая, что изменение помогло, всегда подтверждая, что да.
Производительность — это цикл: измерь, чтобы найти медленную часть, измени её, потом измерь снова, чтобы подтвердить, что изменение реально помогло, — а не просто сдвинуло проблему или сделало хуже. Без второго измерения ты гадаешь, улучшил ли что-то, а это как «оптимизации» тихо делают вещи медленнее. Относись к каждой починке как к гипотезе, что проверяешь, — та же дисциплина, что эвалы для ИИ или тесты для кода.
Интуиция о медленности обычно неверна. Профилируй, чтобы найти, куда реально уходит время, почини это и измерь снова, чтобы подтвердить, — никогда не оптимизируй по догадке.
До оптимизации знай, какой именно «быстро» ты хочешь. Две разные цели постоянно путают, и улучшение одной может ничего не дать другой.
Задержка — это ожидание одной вещи
Сколько занимает один кофе от заказа до чашки — опыт одного клиента, измеренный от начала до конца.
Задержка (latency) — это сколько занимает одна операция от начала до конца: один запрос, одна загрузка страницы, один запрос к базе. Это то, что отдельный пользователь чувствует: ожидание. Когда кто-то говорит «сайт медленный», он почти всегда имеет в виду задержку — промедление между действием и получением ответа. Снижать задержку — это про то, чтобы каждая отдельная вещь происходила быстрее.
Пропускная способность — это сколько обрабатываешь в секунду
Сколько кофе вся кофейня подаёт за час — не сколько занимает один, а общий поток через заведение.
Пропускная способность (throughput) — это сколько работы система обрабатывает за единицу времени: запросов в секунду, задач в минуту. Это про общую ёмкость, а не отдельное ожидание. У системы может быть отличная пропускная способность (обслуживает тысячи разом), пока каждый пользователь всё равно ждёт какое-то время (высокая задержка), или низкая задержка, но ограниченная ёмкость. Это разные цели, и какую ты гонишь, меняет то, что чинишь.
Они разные, и иногда меняются друг на друга
Мотоцикл доставляет одного человека быстрее всех (низкая задержка); автобус перевозит больше всего людей за рейс (высокая пропускная способность). Лучший транспорт зависит от того, что тебе нужно.
Задержка и пропускная способность независимы, и оптимизация одной может навредить другой. Батчинг (batching) — группировка работы вместе — часто поднимает пропускную способность, но добавляет задержку, потому что каждый элемент ждёт батч. Так что надо решить, что важно здесь: страница, обращённая к пользователю, живёт или умирает на задержке; фоновый конвейер данных заботится о пропускной способности. Назвать свою реальную цель не даёт оптимизировать число, что не важно твоим пользователям.
Задержка — это ожидание одной операции; пропускная способность — сколько обрабатываешь в секунду. Это независимые цели — а батчинг часто меняет задержку на пропускную способность.
Производительность не размазана ровно. Почти всегда одна часть доминирует во времени — и оптимизация чего угодно ещё это впустую потраченное усилие, пока не починишь ту одну.
Одна медленная часть обычно доминирует
Цепь ровно настолько крепка, насколько её слабейшее звено, — усиление крепких звеньев не делает ничего; только слабое решает, выдержит ли она.
В большинстве медленных систем единственное узкое место (bottleneck) отвечает за бо́льшую часть времени — один запрос, один медленный сервис, один плохой цикл. Всё остальное уже достаточно быстро. Поэтому гадание так расточительно: оптимизируй часть, что лишь 2% времени выполнения, и лучшее, что можешь выиграть, — 2%. Найди часть, что 80%, и её починка преображает всё. Весь навык — локализовать доминирующую стоимость, а не улучшать быстрые части.
Система быстра ровно настолько, насколько её самый медленный шаг
Шоссе, что свободно, пока не одно перекрытие полосы, где каждая машина ползёт, — скорость всей поездки задана тем одним заторным местом, а не свободными участками.
Запрос, что течёт через десять шагов, ограничен самым медленным. Ускорение девяти быстрых шагов едва двигает итог; медленный шаг задаёт темп. Так что ты целишься конкретно в заторное место. И заметь, что починка одного узкого места часто вскрывает следующее: убери самый медленный шаг, и появляется новый самый медленный. Работа над производительностью — это итеративный поиск и расчистка того, что сейчас является ограничивающим шагом.
Оптимизация в другом месте — впустую потраченное усилие
Полировать трофей, пока у машины спущено колесо, — усилие, потраченное там, где оно ничего не меняет, пока то, что реально тебя останавливает, сидит проигнорированным.
Время, потраченное на оптимизацию чего угодно, кроме узкого места, — по определению время, что не может осмысленно помочь. Оно часто делает хуже, добавляя сложность и баги ради невидимого выигрыша. Это дисциплина, что насаждает измерение: оно не даёт тебе любовно оптимизировать часть, что ты понимаешь, и заставляет на ту, что реально стоит. Чини узкое место, игнорируй остальное — пока остальное не станет узким местом.
Одно узкое место обычно доминирует во времени, и система быстра ровно настолько, насколько её самый медленный шаг. Чини то одно; оптимизация чего угодно ещё впустую, пока оно не станет узким местом.
Когда ты находишь узкое место, починка обычно одна из небольшого числа классических. Эти несколько паттернов отвечают за подавляющее большинство реальных ускорений.
Лучший алгоритм бьёт более быструю машину
Два маршрута в один город: более быстрая машина на длинной извилистой дороге всё равно проигрывает медленной машине на прямом срезе. Маршрут важнее двигателя.
Крупнейшие выигрыши часто идут от лучшего алгоритма или структуры данных, а не от более быстрого железа или микроправок. O(n²)-цикл, заменённый на O(n)-поиск по хеш-таблице, может превратить минуты в миллисекунды — изменение, с которым никакая более быстрая машина не сравнится. Прежде чем оптимизировать мелочь, спроси, не неверен ли подход. Лучшая форма, как показывает курс о структурах данных, — самое дешёвое и крупное ускорение из всех.
Перестань делать столько круговых походов
Делать двадцать отдельных походов в магазин за одним предметом каждый, вместо одного похода со списком, — дорога, а не покупки, съедает твой день.
Огромная доля медленности — слишком много круговых походов (round trips): повторные вызовы к базе или сервису, каждый дёшев в одиночку, но разрушителен в массе. Классика — запрос N+1 (N+1 query): достать список, потом сделать ещё один запрос на каждый элемент, превращая один поход в сотни. Лекарство — доставать оптом, спрашивать всё разом. Снижение числа походов обычно бьёт ускорение каждого, потому что сам круговой поход — это и есть стоимость.
Не делай работу, что можешь пропустить или переиспользовать
Готовить блюдо заново с нуля каждый раз, как кто-то просит, вместо того чтобы держать кастрюлю тёплой, — самая быстрая работа это работа, что не повторяешь.
Ещё две классики. Кэширование (caching) — запоминание дорогого результата, чтобы не пересчитывать, — один из крупнейших рычагов из всех (свой курс). И делать меньше: вычисляй вещи лениво, только когда реально нужны; избегай загрузки данных, что не используешь; пропускай работу, чей результат выбросишь. Самая быстрая операция — та, что ты никогда не запускаешь. Часто лучшая оптимизация не в том, чтобы делать работу быстрее, а в том, чтобы не делать её вовсе.
Большинство ускорений — несколько классик: лучший алгоритм, меньше круговых походов (убей N+1), кэширование дорогих результатов и просто меньше работы. Самая быстрая работа это работа, что пропускаешь.
То, как ты меряешь производительность, может тебе соврать. Среднее — самое утешительное и самое обманчивое число, и научиться смотреть за него — вот что отделяет реальную работу над производительностью от принятия желаемого за действительное.
Среднее прячет медленные случаи
Комната с девятью комфортными людьми и одним в огне имеет приятную среднюю температуру — среднее стирает случай, что реально важен.
Сообщать производительность как среднее (average) опасно успокаивающе: оно смешивает быстрое большинство с медленными немногими в одно счастливое число. Но пользователи не переживают среднее — каждый переживает свой запрос, и медленные чувствуют каждую миллисекунду. Среднее время ответа 200 мс может прятать, что один пользователь из двадцати ждёт пять секунд. Среднее говорит тебе, что система в порядке, пока реальная доля твоих пользователей страдает.
Перцентили показывают реальный опыт
Отчёт, что говорит не просто типичное ожидание, а «самый медленный 1% людей ждал вот столько», — называя плохие переживания вместо того, чтобы усреднить их прочь.
Перцентили (percentiles) схватывают то, что прячет среднее. p50 (медиана) — это типичный случай: половина быстрее. p99 — это медленный хвост: 99% быстрее, так что это примерно худший опыт, что реальный пользователь встречает. Следить за p99, а не только за средним, — вот как ты видишь пользователей, что реально страдают. Хвост — это где живёт боль, а перцентили — это как ты делаешь её видимой.
Хвостовая задержка — это то, что пользователи помнят
Один ужасный обед в ресторане перевешивает дюжину нормальных в чьей-то памяти — худшие переживания, а не среднее, формируют то, как люди тебя судят.
Медленные запросы — хвост — непропорционально формируют то, как пользователи воспринимают твой продукт, потому что плохой опыт прилипает. И в масштабе хвост бьёт больше людей, чем кажется: если каждая страница делает много вызовов, шанс, что хотя бы один медленный, растёт быстро, так что почти каждый пользователь рано или поздно чувствует хвост. Поэтому серьёзные команды ставят цели на p99, а не на среднее: приручить худшие случаи — вот что делает продукт ощутимо стабильно быстрым.
Средние прячут медленные случаи, что пользователи реально чувствуют. Следи за перцентилями — p50 для типичного, p99 для болезненного хвоста — потому что худшие переживания это то, что люди помнят.
Есть режим сбоя, обратный игнорированию производительности: гнаться за ней слишком рано, везде, до того как знаешь, что она важна. Знаменитое предупреждение об этом знаменито не зря.
Преждевременная оптимизация — корень зла
Усиливать каждую стену дома против землетрясения, что может и не прийти, до того как кто-то вообще въехал, — огромное усилие, потраченное на проблему, которой у тебя ещё нет.
Знаменитая фраза — «преждевременная оптимизация — корень всех зол» — предостерегает от оптимизации до того, как знаешь, что́ или нужно ли вообще что-то. Оптимизировать код, что не медлен или даже не часто запускается, не покупает ничего и стоит немало: сложности, багов и времени, украденного у того, что важно. Большинству кода не нужно быть быстрым; ему нужно быть верным и ясным. Работа над скоростью — для частей, что измерение доказывает горячими.
Сперва ясно и верно, быстро там, где важно
Ты пишешь рецепт так, чтобы любой мог следовать, и упрощаешь лишь тот один шаг, что делаешь сотню раз в день, — не каждый шаг, только горячий.
Порядок такой: сделай верным, сделай ясным и сделай быстрым только там, где измерение показывает, что это важно. Ясный код легче оптимизировать потом именно потому, что ты можешь его понять; запутанный «быстрый» код трудно чинить, когда настоящее узкое место появляется в другом месте. Оптимизация под скорость почти всегда стоит читаемости, так что ты тратишь эту цену лишь там, где она покупает реальный, измеренный выигрыш, — а остальное держишь простым.
Но не игнорируй производительность по дизайну
Ты не делаешь сейсмоустойчивым сарай, но и не строишь небоскрёб на песке, — некоторые выборы дёшево взять верно рано и разорительно чинить поздно.
Предупреждение не «никогда не думай о производительности». Выбрать O(n²)-алгоритм там, где O(n) был так же лёгок, или структуру, что не масштабируется, — это ошибка дизайна, за которую дорого заплатишь потом. Баланс: не микрооптимизируй рано, но и не делай архитектурных выборов, что медленны по дизайну и дороги в откате. Возьми большую форму верно дёшево заранее; оптимизируй детали, только когда измерение требует.
Преждевременная оптимизация меняет ясность на скорость, что никому не нужна. Сделай верным и ясным сперва, быстрым лишь там, где измерение доказывает важность, — но не выбирай медленную-по-дизайну форму, о которой пожалеешь.
Производительность, сделанная хорошо, — это спокойный, повторяемый метод, а не героическая суматоха. Вся практика умещается в короткий цикл, что ты гоняешь, только когда есть реальная причина.
Цикл: измерь, почини узкое место, повтори
Механик диагностирует приборами, чинит ту одну отказывающую часть, потом перетестирует — никогда не меняя случайные части в надежде, что шум уйдёт.
Метод прост и дисциплинирован: измерь, чтобы найти узкое место, почини то одно, измерь снова, чтобы подтвердить и вскрыть следующее узкое место, и остановись, когда достаточно быстро. Каждый проход целится в текущую доминирующую стоимость и игнорирует всё остальное. Этот цикл держит работу над производительностью честной и эффективной — ты всегда знаешь, что работаешь над тем, что реально важно, и всегда знаешь, помог ли ты.
Поставь цель, потом остановись
Ты утепляешь дом, пока не станет достаточно тепло, потом останавливаешься — ты не добавляешь утепление вечно за точкой, где кто-то может почувствовать.
У производительности нет естественного конца — всегда можно сделать что-то чуть быстрее. Так что поставь цель («страницы под 300 мс на p99») и остановись, когда достиг. Без цели оптимизация становится бесконечной ямой времени с убывающей отдачей, а сложность вкрадывается ради выигрышей, что никто не замечает. Знать, что значит «достаточно быстро» — для твоих пользователей, твоего случая, — вот что даёт сделать ровно достаточно работы над производительностью и пойти строить что-то ещё.
- Измерил ли я, куда на деле уходит время, или я гадаю? - Задержка или пропускная способность — какой вид быстрого мне реально нужен? - Какое узкое место — та одна часть, что доминирует во времени? - Это классика — плохой алгоритм, слишком много круговых походов, отсутствие кэша, лишняя работа? - Слежу ли я за p99, а не только за средним, что прячет медленный хвост? - Есть ли цель, к которой я оптимизирую, чтобы знать, когда остановиться?
- profiler / измерь-почини-измерь — поиск реальной стоимости и цикл, что подтверждает починку.
- latency / throughput — ожидание одной вещи против объёма в секунду. - узкое место (bottleneck) — та одна медленная часть, что доминирует во времени. - алгоритм / круговые походы / N+1 / кэширование — классические источники медленности и их починки. - average / percentile / p50 / p99 / хвостовая задержка — почему среднее врёт, а хвост бьёт. - преждевременная оптимизация — гнаться за скоростью до того, как измерение доказало нужду. - batching — группировка работы ради пропускной способности ценой задержки.
- Ты измеряешь до того, как что-то трогать, и измеряешь снова после. - Ты чинишь узкое место и игнорируешь части, что уже достаточно быстры. - Твои выигрыши идут от алгоритмов, меньшего числа круговых походов и кэширования, а не от микроправок. - Ты судишь скорость по p99, а не по льстивому среднему. - Ты оптимизируешь к цели и останавливаешься, держа остальное ясным и верным.
Производительность — это спокойный цикл: измерь, почини то одно узкое место, измерь снова, остановись на цели. Тянись к ней лишь там, где доказано, что важно, и держи всё остальное ясным.