Представьте, что в benchmark Go нужно сравнить реализации по числу выделений памяти на одну операцию, а не по времени. Как получить такой показатель?
В benchmark следует вызвать b.ReportAllocs(). После этого тестовый фреймворк Go добавит в результат показатели allocs/op — среднее число выделений памяти на одну операцию — и B/op — средний объём выделенной памяти на операцию.
Глобальная переменная sink не даёт компилятору удалить вызов как неиспользуемый результат. В отчёте benchmark появятся значения ns/op, B/op и allocs/op; последние два показывают стоимость операции с точки зрения памяти.
Обычный benchmark изначально прежде всего отвечает на вопрос о времени выполнения. Для серверных и высоконагруженных приложений этого недостаточно: лишние выделения увеличивают нагрузку на сборщик мусора, задержки и потребление памяти.
Поэтому инструментирование benchmark дополняют измерением аллокаций. Это позволяет отделить быстрый, но создающий много мусора код от решения, которое требует немного больше вычислений, но почти не нагружает сборщик мусора.
Если результат функции в benchmark не используется, компилятор может оптимизировать вычисление или заменить его эквивалентом. Тогда измерение не отражает реальную работу, а показатель выделений может оказаться заниженным или равным нулю.
Даже при использовании результата ручной подсчёт выделений ненадёжен: одно логическое действие может включать несколько внутренних аллокаций, а оптимизации компилятора, escape analysis и особенности рантайма меняют фактическую картину.
b.ReportAllocs() включает сбор статистики памяти для выполняемого benchmark. Фреймворк многократно запускает тело теста, определяет общее количество операций и вычисляет средние значения на одну операцию.
Количество итераций задаётся самим фреймворком через b.N. Оно подбирается так, чтобы измерение было достаточно стабильным, поэтому нельзя трактовать общее число выделений за один конкретный запуск как постоянную величину. Основной результат — нормализованные показатели B/op и allocs/op.
Для запуска можно использовать флаг -benchmem, который также включает вывод статистики памяти для benchmark. Вызов ReportAllocs удобен, когда необходимость измерять аллокации явно зафиксирована в самом тесте.
Важно сохранять результат операции, если он может быть устранён оптимизатором. Обычно применяют пакетную или глобальную переменную-приёмник; локальная переменная, значение которой никак не влияет на наблюдаемый результат, не всегда защищает benchmark от оптимизаций.
Показатель allocs/op не равен числу вызовов make или new в исходном коде. Он отражает фактические выделения, попавшие в измеряемую область, включая последствия escape analysis. Поэтому benchmark нужно запускать в одинаковом окружении и сравнивать реализации одной и той же функциональности.
Команда сравнивала два сериализатора. Первый показывал немного меньшее ns/op, но создавал 12 аллокаций на операцию; второй был на несколько процентов медленнее, зато выполнял одну аллокацию. На нагрузочном тесте первый вариант сильнее увеличивал паузы сборщика мусора.
Рассматривались три подхода. Сравнивать только ns/op было просто, но игнорировало давление на память. Считать вызовы make вручную было понятно по исходному коду, но не учитывало оптимизации компилятора. Включить ReportAllocs и дополнительно проверить общий профиль памяти оказалось наиболее надёжным вариантом.
Выбрали третий подход: benchmark фиксировал B/op и allocs/op, а профиль подтверждал влияние на приложение. В результате команда приняла немного более медленную реализацию с существенно меньшим количеством аллокаций и снизила нагрузку на сборщик мусора.
make заменой allocs/op?make описывает операцию на уровне исходного кода, а не гарантированное выделение в куче. Компилятор может устранить выделение, разместить значение на стеке или объединить действия, если это безопасно. allocs/op измеряет фактический результат работы рантайма в условиях benchmark.
allocs/op, что функция вообще не использует память?Нет. Оно означает, что в измеряемой области не было зафиксировано новых выделений на куче в среднем на операцию. Функция всё ещё может использовать стек, заранее выделенный буфер, память вызывающего кода или данные, созданные вне измеряемого цикла.
Иначе показатель будет включать стоимость подготовки, а не только сравниваемой операции. Например, создание входного буфера внутри цикла исказит B/op и allocs/op, особенно если одна реализация работает с готовыми данными. Подготовку обычно выполняют до цикла benchmark, а её влияние отдельно проверяют только при необходимости.