Вам дали benchmark, в котором результат вычисления нигде не используется. Какой риск это создаёт для достоверности измерения?
package checksum
import "testing"
func sum(data []byte) uint64 {
var result uint64
for _, value := range data {
result += uint64(value)
}
return result
}
func BenchmarkSum(b *testing.B) {
data := make([]byte, 1024)
for i := 0; i < b.N; i++ {
sum(data)
}
}
Результат sum(data) не используется, поэтому компилятор может встроить функцию и удалить вычисление как не имеющее наблюдаемого эффекта. В таком случае benchmark измерит не стоимость суммирования, а в основном накладные расходы цикла или даст искусственно заниженное время.
Чтобы сохранить вычисление, результат обычно записывают в пакетную переменную-sink, например var benchmarkResult uint64. Это делает результат наблюдаемым для компилятора, хотя сама запись в переменную добавляет небольшую измеряемую операцию.
Механизм benchmark в пакете testing предназначен для многократного выполнения операции и сравнения её времени и аллокаций. Количество итераций задаётся фреймворком через b.N, чтобы получить достаточно стабильное измерение.
Такая схема предполагает, что тело цикла действительно выполняет проверяемую работу. Оптимизирующий компилятор не обязан сохранять вычисления, результат которых никак не влияет на наблюдаемое поведение программы.
В примере функция вычисляет сумму, но возвращаемое значение сразу отбрасывается. Если функция будет встроена, компилятор может доказать отсутствие побочных эффектов и убрать весь расчёт.
Это опасно тем, что benchmark может выглядеть очень быстрым и стабильным, хотя измеряемая операция фактически не выполняется. Сравнение двух таких тестов также может привести к неправильному выводу о производительности.
Используют глобальную переменную, в которую записывают результат:
Запись в переменную вне локального контекста создаёт наблюдаемый эффект, поэтому компилятор не может просто удалить вызов sum. Sink не должен использоваться в рабочем коде: он нужен только для корректности benchmark.
Важно, что sink не делает измерение абсолютно чистым. Время присваивания и возможные особенности конкретной версии компилятора остаются частью измерения, но обычно их влияние мало по сравнению с полностью удалённым вычислением. Для коротких операций этот overhead может быть заметным, поэтому полезно сравнивать результаты с осторожностью и увеличивать объём работы на одну итерацию.
Наличие вызова функции само по себе не гарантирует, что работа будет измерена: функцию могут встроить, а мёртвый результат — устранить. При этом оптимизация зависит от кода и версии компилятора, поэтому нельзя утверждать, что удаление произойдёт в каждом конкретном случае; проблема заключается в отсутствии гарантии.
Команда сравнивала две реализации хеширования. В первой benchmark вызывал функцию и игнорировал результат, во второй результат сохранялся в sink. Первая реализация показывала почти нулевое время, но профилирование рабочего приложения подтверждало, что хеширование не является бесплатным.
Рассматривались два варианта. Полностью отключить оптимизации компилятора можно для диагностики, но такие результаты плохо отражают рабочую сборку. Добавить sink проще и сохраняет обычные оптимизации; недостаток — небольшая стоимость записи.
Выбрали sink и проверили benchmark на нескольких размерах входа. После этого результаты стали согласовываться с профилем приложения, а сравнение реализаций стало содержательным.
Нет, если значение остаётся неиспользованным внутри функции. Например, result := sum(data) без дальнейшего чтения всё ещё может быть удалено вместе с вычислением. Sink обычно делают пакетной переменной и присваивают ему результат каждой итерации.
fmt.Println для предотвращения оптимизации?Технически вывод создаёт наблюдаемый эффект, но он несопоставимо дороже измеряемой операции и разрушает смысл benchmark. Вывод также создаёт лишний шум и искажает результаты. Sink решает задачу с гораздо меньшими накладными расходами.
Sink предотвращает одну конкретную ошибку — устранение вычисления компилятором. Он не устраняет влияние аллокаций, сборки мусора, размера входа, прогрева кэшей, ветвления и внешней нагрузки. Поэтому нужно выбирать реалистичные данные, запускать сравнения в одинаковых условиях и интерпретировать результаты как статистическое измерение, а не как точную константу стоимости операции.