Бенчмарки Go и benchmem

Benchmark измеряет малую операцию в контролируемом процессе. Он полезен для сравнения реализаций при фиксированном workload, но не доказывает улучшение latency сервиса без интеграционного измерения.

Зачем это на интервью

Интервьюер ищет дисциплину «гипотеза → baseline → одно изменение → повторение», а не число ns/op без контекста.

Минимум для E4

  • Писать func BenchmarkX(b *testing.B) в _test.go.
  • Выполнять подготовку вне цикла и использовать результат, чтобы компилятор не выкинул работу.
  • Запускать go test -bench '^BenchmarkX$' -benchmem -count=5.
  • Читать ns/op, B/op, allocs/op вместе с входом и correctness test.
var sink int
 
func BenchmarkSum(b *testing.B) {
    data := make([]int, 1024)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        sink = sum(data)
    }
}

Углубление для E5/Senior

b.N подбирается раннером; не задавайте число вручную. Для разных размеров входа применяют sub-benchmarks (b.Run) и b.SetBytes для throughput. b.ReportAllocs() включает allocation metrics программно, -benchmem — для прогона. b.StopTimer/StartTimer допустимы для исключения неизбежимой setup-работы, но не для скрытия реальных затрат операции.

Сравнивайте несколько прогонов на сходном железе и без параллельной нагрузки; для статистического сравнения используют benchstat на сохранённых текстовых результатах. Изменение может улучшить микротест, но увеличить contention, память или сложность API. Поэтому сохраняют baseline, запускают unit tests и при необходимости нагрузочный тест.

Ключевые понятия

МетрикаЧто показывает
ns/opсредняя стоимость одной операции
B/opвыделенные байты на операцию
allocs/opчисло выделений на операцию
b.Nавтоматически выбранное число итераций

Типовые вопросы

  1. Зачем global sink? — Чтобы результат наблюдался и вычисление не было оптимизировано как мёртвое.
  2. Что измеряет -benchmem? — Выделения и байты на операцию, а не retained heap после всего процесса.
  3. Почему одна цифра ненадёжна? — Шум CPU, cache, GC и scheduler требует повторов и сравнимого окружения.
  4. Когда нужен RunParallel? — Когда контракт операции конкурентный; он измеряет другой workload и не заменяет последовательный benchmark.
  5. Почему уменьшение allocs не всегда победа? — Возможны рост CPU, удержания памяти, lock contention или ухудшение читаемости.

Практика

  • Сравните две реализации сериализации на трёх размерах input. Готово: есть sub-benchmarks, -benchmem, пять прогонов и корректность результата.
  • Уберите одну подтверждённую аллокацию. Готово: сохранены до/после, объяснён trade-off, go test ./... проходит.

Частые ошибки и ловушки

  • Генерировать input внутри измеряемого цикла без намерения измерять генерацию.
  • Бенчмаркать I/O без контролируемого сервера и называть результат CPU-скоростью.
  • Делать вывод из одной записи или сравнивать разные версии Go.

Связанные темы

Инструменты Go · Производительность и память · pprof

Источники