Skip to content
...

Бенчмарки

Плагин позволяет сравнивать производительность нескольких реализаций с помощью атрибута #[Bench]#[Bench(array $callables, array $arguments = [], int $warmup = 1, int $calls = 1_000, int $iterations = 10)]Объявляет бенчмарк для сравнения производительности метода с альтернативными реализациями.. Метод с атрибутом выступает эталоном, а альтернативные реализации передаются в параметрах. Testo замеряет время выполнения, собирает статистику и определяет, какая реализация быстрее.

Класс плагина: BenchmarkPlugin\Testo\Bench\BenchmarkPlugin. Входит в SuitePlugins\Testo\Application\Config\Plugin\SuitePlugins по умолчанию.

#[Bench]

Объявляет бенчмарк для сравнения производительности метода с альтернативными реализациями.

#[Bench(array $callables, array $arguments = [], int $warmup = 1, int $calls = 1_000, int $iterations = 10)]

Метод с атрибутом выступает эталоном (current в таблице результатов). Testo замеряет время выполнения всех реализаций, собирает статистику и определяет, какая быстрее.

Параметры:

$callables
Альтернативные реализации для сравнения с эталоном. Ассоциативный массив, где ключ — имя для таблицы результатов, а значение — любой допустимый в PHP callable: [Класс::class, 'метод'], 'функция', замыкание.
$arguments
Аргументы, передаваемые во все функции — и в эталонную, и в альтернативные. Все реализации получают одинаковые входные данные.
$warmup
Количество прогревочных вызовов перед замерами. Устраняет влияние холодного старта (ленивая инициализация, первичные аллокации). Результаты прогрева в замерах не учавствуют.
$calls
Количество вызовов функции за одну итерацию. Для очень быстрых функций (микросекунды) стоит увеличить это значение.
$iterations
Количество повторных замеров. Каждая итерация — независимый запуск всех $calls вызовов, результаты усредняются. Несколько итераций нужны для фильтрации случайных всплесков.

Базовое использование

Допустим, вы хотите узнать, что быстрее: посчитать сумму чисел циклом for или через array_sum. Повесьте атрибут #[Bench]#[Bench(array $callables, array $arguments = [], int $warmup = 1, int $calls = 1_000, int $iterations = 10)]Объявляет бенчмарк для сравнения производительности метода с альтернативными реализациями. на один из методов и укажите второй в callables:

php
#[Bench(
    callables: [
        'sumInArray' => [self::class, 'sumInArray'],
    ],
    arguments: [1, 5_000],
)]
public static function sumInCycle(int $a, int $b): int
{
    $result = 0;
    for ($i = $a; $i <= $b; ++$i) {
        $result += $i;
    }

    return $result;
}

public static function sumInArray(int $a, int $b): int
{
    return \array_sum(\range($a, $b));
}

Метод sumInCycle — это эталон, с которым сравниваются остальные. В callables перечислены альтернативные реализации, а arguments — аргументы, которые получат все функции.

После запуска Testo выведет таблицу с результатами:

Results for sumInCycle:
+----------------------------+----------------------------------------------+---------+
| BENCHMARK SETUP            | TIME RESULTS                                 | SUMMARY |
| Name       | Iters | Calls | Mean             | Median           | RStDev | Place   |
+------------+-------+-------+------------------+------------------+--------+---------+
| current    | 10    | 200   | 37.49µs          | 37.50µs          | ±1.53% | 2nd     |
| sumInArray | 10    | 200   | 11.26µs (-70.0%) | 11.20µs (-70.1%) | ±1.52% | 1st     |
+------------+-------+-------+------------------+------------------+--------+---------+

В столбце Name current обозначает метод с атрибутом (эталон), а sumInArray — альтернативную реализацию. Процент в скобках показывает, насколько реализация быстрее или медленнее эталона: (-70.0%) означает, что sumInArray выполнилась на 70% быстрее. Столбец Place — итоговое место в рейтинге.

Результаты

Таблица результатов разделена на три группы столбцов:

BENCHMARK SETUP — параметры запуска:

СтолбецОписание
NameИмя реализации. current обозначает эталонный метод.
ItersСколько итераций было выполнено.
CallsСколько раз функция вызывалась за одну итерацию.

TIME RESULTS — результаты замеров:

СтолбецОписание
MeanСреднее арифметическое по всем итерациям. Процент в скобках — разница относительно эталона.
MedianМедиана. В отличие от среднего, на неё не влияют единичные аномально быстрые или медленные запуски.
RStDevОтносительное стандартное отклонение — показывает, насколько стабильны замеры между итерациями. Чем меньше, тем лучше.

SUMMARY — итог:

СтолбецОписание
PlaceИтоговое место в рейтинге. Первое место — самая быстрая реализация.

Расширенная таблица

При достаточном количестве итераций Testo может показать расширенную статистику с автоматической фильтрацией аномальных замеров (выбросов). К базовым группам добавляется FILTERED RESULTS:

+----------------------------+-------------------------------------------------+------------------------------------+--------------------------------------------------------------+
| BENCHMARK SETUP            | TIME RESULTS                                    | FILTERED RESULTS                   | SUMMARY                                                      |
| Name       | Iters | Calls | Mean              | Median            | RStDev  | Rej. | Mean*             | RStDev* | Place | Warnings                                             |
+------------+-------+-------+-------------------+-------------------+---------+------+-------------------+---------+-------+------------------------------------------------------+
| current    | 10    | 20    | 44.03µs           | 43.68µs           |  ±2.35% | 1    | 43.69µs           |  ±0.42% | 3rd   |                                                      |
| calcBar    | 10    | 20    | 13.72µs (-68.8%)  | 13.26µs (-69.6%)  |  ±7.77% | 2    | 13.23µs (-69.7%)  |  ±0.52% | 2nd   |                                                      |
| calcBaz    | 10    | 20    | 110.50ns (-99.7%) | 105.00ns (-99.8%) | ±16.50% | 1    | 106.11ns (-99.8%) | ±12.52% | 1st   | High variance, low iter time. Insufficient iter time |
+------------+-------+-------+-------------------+-------------------+---------+------+-------------------+---------+-------+------------------------------------------------------+

К базовым столбцам добавляются:

СтолбецОписание
Rej.Сколько итераций Testo отбросил как выбросы — они значительно отклонялись от остальных и искажали статистику.
Mean*Среднее после удаления выбросов. Именно на эти значения стоит ориентироваться.
RStDev*Отклонение после удаления выбросов.
WarningsПредупреждения о качестве данных — например, что замеры нестабильны или время выполнения слишком мало для точного измерения.

Если Testo обнаружил проблемы с качеством данных, под таблицей появятся рекомендации:

Recommendations:
  ⚠ High variance, low iter time: Measurement overhead may dominate — increase calls per iteration.
  ⚠ Insufficient iter time: Timer jitter exceeds useful signal — increase calls per iteration.
Как читать таблицу результатов?

Строка current — метод с атрибутом #[Bench]#[Bench(array $callables, array $arguments = [], int $warmup = 1, int $calls = 1_000, int $iterations = 10)]Объявляет бенчмарк для сравнения производительности метода с альтернативными реализациями. (эталон), остальные — альтернативные реализации.

  1. Посмотрите на столбец Rej. — сколько итераций отброшены как аномальные. Если больше одной-двух, результатам пока доверять рано: увеличьте calls или iterations, закройте лишние процессы и перезапустите тест. Если столбца нет, значит выбросов не было.
  2. Проверьте RStDev* (или RStDev, если расширенной таблицы нет). Ориентир — менее 2%. Если выше, то замеры ещё недостаточно стабильны.
  3. Сравнивайте реализации по Mean* (или Mean) — это среднее время выполнения, очищенное от аномалий. Процент в скобках показывает разницу относительно эталона: отрицательный — быстрее, положительный — медленнее.

Стабильность результатов

Каждый перезапуск бенчмарков может выдавать слегка отличающиеся результаты — это нормально. Фоновые процессы, активность операционной системы и другие факторы влияют на производительность в конкретный момент. Чтобы делать выводы, нужны стабильные замеры.

Стабильность оценивается по столбцу RStDev (относительное стандартное отклонение). Он показывает, насколько сильно результаты итераций разбросаны относительно среднего. Ориентир — RStDev < 2%: при таком разбросе можно уверенно говорить, что одна реализация быстрее другой.

Если RStDev слишком высокий, есть два способа его снизить:

  • Увеличить iterations. Больше повторных замеров — больше данных для усреднения. Это помогает, когда нестабильность вызвана внешними факторами вроде фоновой нагрузки на систему.
  • Увеличить calls. Если функция выполняется за микросекунды, время одного вызова может быть сопоставимо с погрешностью самого таймера. Увеличив количество вызовов за итерацию, вы получите более длительный и точный замер.

Для быстрых функций (микросекунды) начинайте с увеличения calls. Для более медленных (миллисекунды и выше) обычно достаточно увеличить iterations.

Testo автоматически отбрасывает аномальные замеры (выбросы) и пересчитывает статистику без них. Столбцы Mean* и RStDev* в расширенной таблице показывают результат после такой фильтрации.

Использование в CI

Бенчмарк с атрибутом #[Bench]#[Bench(array $callables, array $arguments = [], int $warmup = 1, int $calls = 1_000, int $iterations = 10)]Объявляет бенчмарк для сравнения производительности метода с альтернативными реализациями. — это полноценный тест, который можно запускать в CI. Если эталонная реализация оказалась медленнее любой из альтернатив, тест будет считаться проваленным.

Допустим, вы написали свой сериализатор вместо стандартного json_encode, потому что он быстрее на ваших структурах данных. Бенчмарк фиксирует это как факт. Если после рефакторинга ваша реализация перестанет быть быстрее стандартной — значит, что-то изменилось и стоит разобраться, пока это не ушло в продакшен.

php
// Наш сериализатор должен быть быстрее стандартного json_encode.
// Если это перестанет быть правдой — тест упадёт.
#[Bench(
    callables: [
        'json_encode' => [self::class, 'viaJsonEncode'],
    ],
    arguments: [new UserDto(name: 'John', age: 30)],
    calls: 1000,
    iterations: 5,
)]
public static function serialize(UserDto $dto): string
{
    return DtoSerializer::serialize($dto);
}

public static function viaJsonEncode(UserDto $dto): string
{
    return json_encode($dto);
}

Результаты бенчмарков зависят от окружения. На CI-серверах с общими ресурсами разброс результатов может быть выше, чем на локальной машине, поэтому стоит использовать большие значения iterations и calls для надёжности.