Бенчмарки и профилирование в Rust
Завершающий блок: как измерять. Спрашивают, почему бенчмарк показал ноль, зачем criterion и чем бенчмарк отличается от профилирования.
Стержень: бенчмарк отвечает «стало ли быстрее», профилировщик - «где именно тратится время»; путать их бессмысленно.
// Формулировки: «почему замер дал ноль наносекунд?», «зачем criterion?», «как искать горячие места?»
Почему замеры врут
Первая причина - debug-сборка: без инлайнинга и оптимизаций, зато с проверками переполнения, меняется не только время, но и профиль. Горячим окажется совсем не то место, что в проде.
Вторая - оптимизатор. Если результат вычисления никому не нужен, а вход константный, всё свернётся ещё при компиляции, и бенчмарк покажет ноль. Спасает black_box: он прячет значение от оптимизатора и на входе, и на выходе.
// Третья - шум: холодный кэш, частота процессора, соседние процессы. Один прогон через Instant::now ничего не доказывает.
- black_box
- барьер, скрывающий значение от оптимизатора
- свёртка констант
- вычисление выражения на этапе компиляции
Инструменты под задачу
criterion берёт на себя всё перечисленное: прогрев, множество итераций, статистику по распределению и сравнение с предыдущим прогоном - он прямо говорит, изменение значимо или это шум. Это ответ на вопрос «стало ли быстрее».
Вопрос «где тратится время» решает семплирующий профилировщик: он снимает стеки вызовов много раз в секунду, а флеймграф собирает их в картину, где ширина полосы - доля времени. Ручные таймеры годятся только для проверки готовой гипотезы, а сами гипотезы обычно неверны.
// Отдельный сюжет - хвост задержек. Разрыв между медианой и p99 делают не вычисления, а ожидания: очередь за замком, нехватка воркеров, всплеск аллокаций, блокирующий вызов в асинхронной задаче. Ищут это трассировкой по стадиям, а не профилем CPU.
- criterion
- бенчмарки со статистикой и сравнением прогонов
- флеймграф
- картина стеков по доле занятого времени
Как отвечать: «Бенчмарк показал ноль наносекунд. Что произошло?»
Оптимизатор выбросил вычисление. Если результат нигде не используется, а вход - константа, всё сворачивается ещё при компиляции, и мерить оказывается нечего. Лечится black_box вокруг входных данных и результата: он прячет значения от оптимизатора и заставляет код реально выполняться. Именно поэтому я беру criterion, а не пишу замер руками: там уже есть и прогрев, и множество итераций, и статистика, которая отличает реальное изменение от шума. И меряю всегда на release, потому что debug даёт не только другие числа, но и другой профиль.
Ответ показывает понимание того, что мешает измерению, а не знание названия библиотеки. Про release и профиль - деталь, которую упускают почти все.
На чём валятся
- −Меряют на debug-сборке.
- −Забывают black_box и получают нулевое время «идеальной» функции.
- −Делают один замер через Instant::now и делают выводы по шуму.
- −Ищут горячие места догадками вместо профилировщика.
- −Оптимизируют среднее, когда пользователи жалуются на хвост.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 11, остальные разбираются в тренажёре.
- Что даёт criterion по сравнению с ручным замером через Instant::now()?A)Прогрев, много итераций и статистику по изменениямB)Возможность запускать бенчмарки на стабильном компилятореC)Профиль по строкам кода с указанием горячих местD)Замер потребления памяти вместе со временем выполнения
показать ответ и разбор
+A)Прогрев, много итераций и статистику по изменениям// разбор: Один замер ничего не говорит: мешают холодный кэш, частота процессора, соседние процессы. criterion прогревает код, гоняет множество итераций, считает распределение и сравнивает с прошлым прогоном, отделяя реальное изменение от шума. Горячие места он не показывает — для этого нужен профилировщик.
- Сервис держит нагрузку, но p99 в пять раз хуже медианы. Куда смотреть?A)На алгоритмическую сложность горячей функцииB)На среднюю загрузку процессора: при высокой она растягивает все запросыC)На размер бинарника: он влияет на промахи кэша инструкцийD)На блокировки, ожидание в пуле и всплески аллокаций
показать ответ и разбор
+D)На блокировки, ожидание в пуле и всплески аллокаций// разбор: Хвост задержек обычно делают не вычисления, а ожидание: очередь за мьютексом, нехватка воркеров в пуле, всплеск перевыделений, редкий поход в холодный кэш или блокирующий вызов внутри асинхронной задачи. Медиана таких эффектов не показывает — нужны гистограммы по стадиям и трассировка отдельных запросов.
- Как обычно ищут горячие места в Rust-сервисе под нагрузкой?A)Семплирующим профилировщиком и флеймграфом по стекамB)Прогоном юнит-тестов с включённым таймеромC)Печатью времени вокруг подозрительных функцийD)Сравнением размеров бинарника до и после изменений
показать ответ и разбор
+A)Семплирующим профилировщиком и флеймграфом по стекам// разбор: Семплирование снимает стеки много раз в секунду и показывает, где программа реально проводит время; флеймграф собирает их в картину, где ширина полосы — доля времени. Ручные таймеры годятся для проверки гипотезы, но сами гипотезы обычно неверны — узкое место почти всегда не там, где кажется.
- Что делает black_box в бенчмарке?A)Замеряет время выполнения переданного выражения с точностью до наносекундB)Запускает выражение в отдельном потоке, изолируя его от остального кодаC)Скрывает значение от оптимизатора, чтобы вычисление не выбросили как ненужноеD)Прогревает кэш процессора перед основной серией измерений в бенчмарке
показать ответ и разбор
+C)Скрывает значение от оптимизатора, чтобы вычисление не выбросили как ненужное// разбор: Оптимизатор вправе выбросить вычисление, результат которого никому не нужен, — и микробенчмарк начинает показывать ноль наносекунд. black_box делает значение непрозрачным: компилятор перестаёт рассуждать о нём и вынужден выполнить работу. Оборачивают и вход, чтобы константы не свернулись при компиляции, и результат, чтобы вычисление не удалили.
- Бенчмарк показывает разброс в 30% между прогонами. Что делать до того, как верить числам?A)Взять минимальное из измерений: оно ближе всего к чистому времени выполненияB)Сравнивать только среднее арифметическое: оно устойчиво к случайным выбросамC)Отключить оптимизации, чтобы убрать влияние компилятора на разброс результатовD)Увеличить число итераций и смотреть на распределение, а не на одно число
показать ответ и разбор
+D)Увеличить число итераций и смотреть на распределение, а не на одно число// разбор: Источники шума известны: соседние процессы, изменение частоты процессора, состояние кэша, случайные страничные промахи. Одно число из такого замера ничего не значит. criterion гоняет серию, показывает распределение и доверительный интервал, а также сравнивает с прошлым прогоном — по этому и судят. Отключать оптимизации бессмысленно: мерить надо то, что попадёт в прод.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.