В benchmark нужно вывести средний размер обработанных данных на одну операцию. Какое значение фактически покажет этот код и какой механизм ReportMetric к этому приводит?
package codec
import "testing"
func BenchmarkEncode(b *testing.B) {
data := make([]byte, 4096)
total := 0
b.ResetTimer()
for i := 0; i < b.N; i++ {
total += len(data)
}
b.ReportMetric(float64(total), "bytes/op")
}
Код покажет примерно 4096 bytes/op, а не 4096 * b.N. Если единица пользовательской метрики заканчивается на /op, пакет testing автоматически делит переданное значение на b.N.
Следовательно, передавать нужно суммарное значение за все итерации: total корректен. Если заранее вычислить среднее на операцию, автоматическое деление даст результат, заниженный ещё в b.N раз.
Стандартный benchmark Go по умолчанию сообщает время на операцию и количество операций. На практике этого недостаточно: для кодеков, сериализаторов и сетевых обработчиков важны объём данных, пропускная способность или другие доменные показатели.
ReportMetric позволяет добавить такую метрику без отдельной системы измерений. Суффикс /op задаёт стандартному инструменту правило нормализации значения к одной операции.
Цикл benchmark выполняется b.N раз, причём значение b.N выбирает сам фреймворк и может менять между запусками для достижения подходящей длительности измерения. Переменная total после цикла содержит объём, накопленный за все итерации.
Если разработчик не учитывает автоматическое деление, он может получить неверную пропускную способность. Особенно опасна двойная нормализация: среднее значение сначала вычисляют вручную, а затем передают с единицей, оканчивающейся на /op.
b.ReportMetric(value, unit) регистрирует пользовательскую метрику. Когда unit имеет суффикс /op, Go интерпретирует value как суммарное значение за benchmark и делит его на b.N перед выводом.
В примере каждая итерация добавляет 4096 байт, поэтому после цикла total равен 4096 * b.N. После нормализации получается 4096 bytes/op.
Корректный вариант:
Некорректный вариант — заранее передать float64(processed) / float64(b.N) с той же единицей bytes/op: testing разделит это значение ещё раз. Если значение уже нормализовано вручную, можно использовать единицу без суффикса /op, но тогда ответственность за смысл и масштаб метрики полностью лежит на коде benchmark.
ReportMetric не заменяет b.SetBytes: это разные механизмы. SetBytes задаёт размер данных для расчёта стандартной пропускной способности, а ReportMetric добавляет произвольную именованную метрику.
Команда сравнивала два сериализатора. Для каждого benchmark вычислялся размер результата на одной итерации, после чего значение передавалось как bytes/op. В отчёте один сериализатор якобы обрабатывал почти нулевой объём данных.
Рассматривались два варианта. Можно было убрать суффикс /op и вручную рассчитывать среднее, но это повышало риск несогласованного формата отчётов. Можно было передавать сумму за все итерации и оставить автоматическую нормализацию.
Выбрали второй вариант: benchmark накапливал общий объём и вызывал ReportMetric(total, "bytes/op"). После этого результаты стали сопоставимыми независимо от автоматически выбранного b.N, а формула нормализации осталась в одном месте — в стандартном фреймворке.
1. Что произойдёт, если передать 4096 с единицей bytes/op напрямую?
Фреймворк разделит 4096 на b.N, поэтому отчёт будет показывать примерно 4096 / b.N bytes/op. Это не означает, что одна операция обрабатывает такой маленький объём; ошибка возникла из-за передачи уже нормализованного или неполного значения.
2. Можно ли использовать единицу bytes вместо bytes/op, чтобы избежать деления?
Да, автоматическое деление по суффиксу /op тогда не применяется. Но значение будет обозначать суммарный объём за весь benchmark, а не объём одной операции. Такой формат легко неправильно интерпретировать и сложнее сравнивать между запусками с разными b.N, поэтому для среднего на операцию лучше передавать сумму с bytes/op.
3. Почему результат нельзя считать точным, если total зависит от размера входа, а размер входа меняется внутри цикла?
ReportMetric разделит только общий итог на общее число операций. При неодинаковых размерах это будет средний объём на итерацию, но не обязательно показатель для конкретного размера входа. Для корректного сравнения нужно фиксировать размер данных в benchmark или запускать отдельные sub-benchmark для каждого размера.