Нужно сравнить в Go benchmark пропускную способность обработки данных, а не только длительность итерации. К...

Нужно сравнить в Go benchmark пропускную способность обработки данных, а не только длительность итерации. Как подготовить такой benchmark, чтобы результат показывал объём обработанных данных в единицу времени?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Укажите через b.SetBytes количество байт, обрабатываемых за одну итерацию benchmark. Тогда стандартный вывод Go дополнительно покажет пропускную способность, обычно в MB/s. Подготовку данных следует выполнить до измерения, например сбросив таймер через b.ResetTimer.

Исторический контекст

Базовый результат benchmark в Go выражается прежде всего через время одной операции: ns/op. Этого достаточно для сравнения задержки, но неудобно, когда операции обрабатывают разные объёмы данных.

Метаданные о количестве байт за итерацию позволяют нормализовать результат и сравнивать именно скорость потока данных, например при тестировании кодеков, хеширования или копирования буферов.

Постановка проблемы

Если две реализации обрабатывают разные объёмы данных за итерацию, одно сравнение ns/op может быть misleading: более медленная по времени итерация способна обрабатывать существенно больше данных. Без указания размера входа benchmark не знает, как преобразовать время в пропускную способность.

Важно не включать создание входных данных в измеряемую операцию. Иначе результат будет отражать не только производительность проверяемого алгоритма, но и стоимость подготовки окружения.

Подробное решение

Вызов b.SetBytes сообщает фреймворку, сколько байт приходится на одну итерацию. Он не меняет цикл, не ускоряет код и не измеряет фактический объём автоматически: разработчик должен передать корректное значение.

После этого Go использует указанное число байт вместе с измеренным временем и выводит дополнительную метрику пропускной способности. Для корректного сравнения у вариантов должны совпадать объём данных на итерацию, характер входа и условия выполнения.

Минимальный пример:

func BenchmarkCopy(b *testing.B) { src := make([]byte, 64<<10) dst := make([]byte, len(src)) b.SetBytes(int64(len(src))) b.ResetTimer() for i := 0; i < b.N; i++ { copy(dst, src) } }

Здесь одна итерация копирует 64 KiB, поэтому SetBytes получает именно это значение. Выделение буферов выполняется до ResetTimer и не попадает в основное измерение, а вызов copy измеряется в каждой итерации.

SetBytes корректен, когда объём работы на итерацию известен и стабилен. Если количество обработанных байт меняется от итерации к итерации, фиксированная метрика может быть неточной; в таком случае лучше отдельно рассчитывать и публиковать пользовательскую метрику через возможности benchmark API либо привести нагрузку к одинаковому объёму.

Ситуация из практики

Команда сравнивала два декодера: первый обрабатывал один большой буфер за итерацию, второй — несколько маленьких. При сравнении только ns/op второй выглядел быстрее, хотя за то же время обрабатывал меньше данных.

Рассматривались два варианта. Можно было вручную делить время на размер входа после запуска benchmark, но это усложняло анализ и повышало риск ошибки. Другой вариант — сделать одинаковый размер входа, однако это не отражало реальный режим работы декодеров.

Выбрали benchmark с одинаковым сценарием нагрузки и SetBytes, где указывался фактический объём данных на итерацию. В результате сравнение стало выражаться одновременно через задержку операции и пропускную способность; при этом подготовка буферов была вынесена за пределы таймера.

Что кандидаты часто упускают

  1. Вопрос: Влияет ли вызов SetBytes на число итераций, которое выбирает benchmark-фреймворк?

    Ответ: Нет. Он только задаёт размер работы, соответствующий одной итерации, для расчёта дополнительной метрики. Значение b.N по-прежнему выбирается и изменяется фреймворком для получения устойчивого измерения времени.

  2. Вопрос: Можно ли передать в SetBytes размер исходного буфера, если алгоритм фактически обрабатывает только его часть?

    Ответ: Нет, если требуется достоверная пропускная способность. Нужно указывать объём, который действительно обрабатывается за одну итерацию. Иначе показатель MB/s будет завышен или занижен относительно реальной работы алгоритма.

  3. Вопрос: Почему высокий показатель MB/s не доказывает, что реализация лучше во всех сценариях?

    Ответ: Пропускная способность зависит от размера и содержимого входа, распределения памяти, кэширования, аллокаций и особенностей конкретной операции. Реализация может выигрывать на больших буферах за счёт амортизации накладных расходов, но проигрывать на маленьких. Поэтому benchmark должен соответствовать рабочей нагрузке, а вывод следует подтверждать несколькими реалистичными размерами входа.