Бенчмарки
Плагин позволяет сравнивать производительность нескольких реализаций с помощью атрибута #[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:
#[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)]Объявляет бенчмарк для сравнения производительности метода с альтернативными реализациями. (эталон), остальные — альтернативные реализации.
- Посмотрите на столбец Rej. — сколько итераций отброшены как аномальные. Если больше одной-двух, результатам пока доверять рано: увеличьте
callsилиiterations, закройте лишние процессы и перезапустите тест. Если столбца нет, значит выбросов не было. - Проверьте RStDev* (или RStDev, если расширенной таблицы нет). Ориентир — менее 2%. Если выше, то замеры ещё недостаточно стабильны.
- Сравнивайте реализации по 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, потому что он быстрее на ваших структурах данных. Бенчмарк фиксирует это как факт. Если после рефакторинга ваша реализация перестанет быть быстрее стандартной — значит, что-то изменилось и стоит разобраться, пока это не ушло в продакшен.
// Наш сериализатор должен быть быстрее стандартного 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 для надёжности.