Как в Go benchmark отделить время подготовки тестовых данных от времени измеряемой операции?
Подготовку, которую не требуется измерять, выполняют до запуска цикла benchmark, затем вызывают b.ResetTimer(). Этот вызов обнуляет накопленное время и счётчики аллокаций, после чего запускает измерение заново. Если подготовка должна повторяться перед каждой итерацией, её временно исключают через b.StopTimer() и b.StartTimer().
Здесь makeInput не входит в результат benchmark, а измеряется только Encode. Присваивание результата необходимо, если без него компилятор потенциально сможет устранить вычисление как неиспользуемое.
Benchmarking появился как часть стандартного пакета testing, чтобы измерять производительность в том же окружении, где выполняются unit-тесты. Исходная проблема состоит в том, что время подготовки входных данных, настройки окружения и самой операции часто имеет разную смысловую ценность.
Фреймворк запускает benchmark многократно и подбирает значение b.N, поэтому ручное измерение фиксированного числа повторений недостаточно надёжно. Управление таймером позволяет явно определить границу измеряемой работы.
Если подготовка данных выполняется внутри измеряемого участка, в результат попадут парсинг, выделение памяти, чтение файлов или создание объектов. Тогда benchmark будет характеризовать не целевую операцию, а их совокупность.
Обратная ошибка тоже возможна: если реальное приложение каждый раз создаёт входные данные, а benchmark исключает эту часть, результат окажется слишком оптимистичным. Неправильно выбранная граница измерения приводит к неверным решениям об оптимизации.
Одноразовую подготовку размещают до цикла и после неё вызывают b.ResetTimer(). Вызов сбрасывает накопленное время и статистику аллокаций, затем таймер снова начинает работать. Поэтому подготовка не влияет на показатели времени и памяти текущего измерения.
Для повторяющейся, но нецелевой подготовки используют такую схему: остановить таймер, выполнить подготовку, запустить таймер, выполнить измеряемую операцию. Однако частое переключение таймера само усложняет benchmark, поэтому сначала следует проверить, нельзя ли вынести подготовку за цикл.
Подготовку нельзя исключать автоматически. Если она является частью пользовательского сценария, её следует оставить в измеряемом участке или проводить отдельный benchmark для полного сценария. Иногда полезны два benchmark: один для чистой операции, другой для операции вместе с подготовкой.
b.N выбирается инфраструктурой benchmark и может изменяться между запусками. Нельзя делать выводы о производительности по одному запуску или сравнивать тесты с разными объёмами работы без понимания того, что именно измеряется.
Следует также учитывать побочные эффекты: повторное использование изменяемого входа может сделать последующие итерации неэквивалентными, а отсутствие наблюдаемого результата — позволить оптимизатору удалить вычисление. При необходимости применяют проверяемый внешний результат, например пакетную переменную-накопитель, не добавляя лишнюю работу в измеряемый участок.
Команда измеряла сериализацию большого объекта. В первом варианте объект создавался внутри цикла, поэтому benchmark показывал высокое время и число аллокаций. Вынести создание объекта за цикл было быстро, но это исключало из оценки сценарий, где каждый запрос получает новый объект.
Рассматривались три варианта. Измерять всё вместе было реалистично, но не позволяло понять стоимость самой сериализации. Создавать объект один раз и вызывать b.ResetTimer() давало чистую оценку сериализации, но не отражало полный путь запроса. Останавливать таймер на время создания объекта перед каждой итерацией позволяло отдельно измерять сериализацию, однако усложняло тест и не давало единой оценки пользовательского сценария.
Выбрали два benchmark: один измерял сериализацию уже подготовленного объекта, второй — полный сценарий запроса. В результате команда увидела, что оптимизация сериализатора почти не меняет задержку полного запроса: основное время занимала подготовка объекта. Это предотвратило оптимизацию не того участка системы.
Что произойдёт, если подготовка создаёт фоновые горутины перед b.ResetTimer()?
Их работа не обязательно прекратится после сброса таймера. Если горутины продолжают конкурировать за процессор, память или общие блокировки во время измеряемой операции, результат будет загрязнён. Перед ResetTimer нужно дождаться завершения фоновой работы либо явно включить её влияние в сценарий, который измеряется.
Почему нельзя бездумно готовить данные в зависимости от b.N?
b.N подбирается benchmark-фреймворком и меняется между запусками. Если объём подготовки пропорционален b.N, а сама подготовка исключена через ResetTimer, потребление памяти и давление на кэш могут всё равно повлиять на последующие измерения. Размер входа следует задавать отдельно и явно, чтобы сравнивать операции при одинаковых условиях.
Когда повторное использование одного входного объекта искажает результат?
Если измеряемая функция изменяет вход, итерации начинают работать с разными состояниями. Например, первая сериализация может очищать буфер или менять внутренний кэш, и последующие вызовы окажутся дешевле либо будут выполнять другую работу. В таком случае нужно восстанавливать состояние или создавать независимый вход на каждой итерации; если создание исключают из таймера, отдельно проверяют, что это соответствует цели benchmark.