🤔 Задача
Нужно написать функцию, которая посчитает сумму всех чисел от $a до $b.
Например, если $a = 1 и $b = 5, то результат будет 1 + 2 + 3 + 4 + 5 = 15.
- Самое простое решение, которое приходит на ум:
в циклеforитеративно прибавлять к$aзначение$i, пока не упрёмся в$b, но слишком банально. - Полёт фантазии не остановить и хочется решить задачу массивами:
заполнить массив значениями от$aдо$bи отправить в функциюsum().

Сравнение решений
PHP очень хорош в синтетических тестах, обгоняет Python и всё такое. А с включённым JIT и некоторыми хитростями может даже догнать C++.
Мы не будем сейчас сравнивать PHP с другими языками, а попробуем просто сравнить эти два варианта решения задачи между собой.
Я рассуждаю так:
Решение на массивах будет медленнее цикла
for, ведь дополнительные ресурсы уходят на вычисление хешей в хеш-таблице при создании массива, а ещё нужно больше памяти для промежуточных значений.
Давайте в этом убедимся: напишем функции и добавим атрибут #[Bench]#[Bench(array $callables, array $arguments = [], int $warmup = 1, int $calls = 1_000, int $iterations = 10)]Объявляет бенчмарк для сравнения производительности метода с альтернативными реализациями. на одну из функций.
#[Bench(
callables: [
'in_array' => [self::class, 'sumInArray'],
],
arguments: [1, 5_000],
calls: 100,
iterations: 1,
)]
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));
}С помощью атрибута #[Bench]#[Bench(array $callables, array $arguments = [], int $warmup = 1, int $calls = 1_000, int $iterations = 10)]Объявляет бенчмарк для сравнения производительности метода с альтернативными реализациями. мы говорим Testo, что:
- хотим сравнить производительность текущей функции (
sumInCycle) с другой функцией (sumInArray); - в обе функции будут передаваться одинаковые аргументы:
1и5_000; - для замера времени каждая функция будет запущена 100 раз подряд (
calls: 100).
Ещё раз делаем ставки и запускаем.
Summary:
+---+-------------+-------+-------+--------+-------------------+
| # | Name | Iters | Calls | Memory | Avg Time |
+---+-------------+-------+-------+--------+-------------------+
| 2 | current | 1 | 100 | 0 | 38.921ms |
| 1 | sumInArray | 1 | 100 | 0 | 21.472ms (-44.8%) |
+---+-------------+-------+-------+--------+-------------------+sumInArray занимает первое место, справившись с задачей почти в два раза быстрее, чем sumInCycle.
Стоп, что? Функция на массивах выиграла с большим отрывом?!

Статистический артефакт
И действительно, это может быть просто "статистический артефакт".
Каждый перезапуск бенчмарков выдаёт разные результаты, иногда даже сильно отличающиеся от предыдущих. Это может быть связано с фоновыми задачами, действиями пользователя или другими явлениями, которые могут повлиять на производительность в моменте.
⚠️ Нам нужны гарантии, что мы сравниваем не просто случайные выбросы, а действительно стабильные результаты.
На помощь приходит статистика в лице коэффициента вариации, который показывает относительную изменчивость данных. Чем меньше этот коэффициент, тем стабильнее результаты.
Всё, что нам нужно сделать, — это получить больше данных, размазанных во времени, то есть перезапустить бенчмарки несколько раз. В атрибуте #[Bench]#[Bench(array $callables, array $arguments = [], int $warmup = 1, int $calls = 1_000, int $iterations = 10)]Объявляет бенчмарк для сравнения производительности метода с альтернативными реализациями. есть параметр iterations, который и отвечает за количество перезапусков бенчмарков.
Выставим iterations: 10 и перезапустим:
Summary:
+---+-------------+-------+-------+--------+-------------------+---------+
| # | Name | Iters | Calls | Memory | Avg Time | RStDev |
+---+-------------+-------+-------+--------+-------------------+---------+
| 2 | current | 10 | 100 | 0 | 38.474ms | ±2.86% |
| 1 | sumInArray | 10 | 100 | 0 | 12.501ms (-67.5%) | ±27.20% |
+---+-------------+-------+-------+--------+-------------------+---------+Теперь sumInArray выполняется в 3 раза быстрее, но коэффициент вариации (столбец RStDev) составляет 27.2%, что довольно много. Чтобы говорить о стабильности результатов, обычно стремятся к RStDev < 2%.
Давайте подумаем, как можно снизить эту вариацию. Наши функции выполняются достаточно быстро, и даже небольшие флуктуации в производительности могут сильно влиять на результаты, особенно при небольшом количестве запусков. Для быстрого кода, как у нас, может помочь увеличение количества calls в каждой итерации. Повышаем до 2000:
Summary:
+---+-------------+-------+-------+--------+-------------------+--------+
| # | Name | Iters | Calls | Memory | Avg Time | RStDev |
+---+-------------+-------+-------+--------+-------------------+--------+
| 2 | current | 10 | 2000 | 0 | 37.888ms | ±1.38% |
| 1 | sumInArray | 10 | 2000 | 0 | 11.395ms (-69.9%) | ±1.72% |
+---+-------------+-------+-------+--------+-------------------+--------+Как вы уже поняли, при такой разнице в производительности (в три раза) даже ±27.2% вариации не спасло бы цикл for от поражения. Но теперь мы можем с уверенностью говорить о стабильности результатов (RStDev < 2%).

Кстати, вы заметили, что в обоих случаях memory=0? Это значит, что для массивов дополнительная память не была аллоцирована и хватило того, что уже было выделено на момент запуска бенчмарков.
Конечно, можно поиграться с более крупным диапазоном, включить JIT и доказать, что в каких-то случаях цикл будет быстрее, но я хочу обратить ваше внимание на то, как быстро теперь можно что-то забенчить!
Bench

Бенчим прямо в коде без лишней обвязки. Как встроенные тесты, только бенчмарки.
Когда-то Dragon Code показал, что бенчмарки могут быть простыми и удобными: вместо тонны обвязки достаточно вызвать один класс и передать замыкания для сравнения. Testo вывел это на новый уровень: от намерения до результата всего один атрибут.
Запускается простым кликом в IDE: 
Но это ещё не всё. За простотой снаружи скрываются серьёзные алгоритмы, подкреплённые статистикой.
Testo автоматически находит отклонения в данных, отметает выбросы и выдаёт метрики, которые помогают понять, насколько стабильны результаты. Для тех, кому цифры мало о чём говорят, есть саммари с рекомендациями и алертами.
Сейчас это выглядит так:
Results for calcFoo:
+----------------------------+-------------------------------------------------+------------------------------------+--------------------------------------------------------------+
| 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 |
+------------+-------+-------+-------------------+-------------------+---------+------+-------------------+---------+-------+------------------------------------------------------+
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.Знаю, выглядит перегружено, но это ещё не релиз. В будущем я вижу это ещё проще, чем сейчас: атрибут с автоматическими настройками без необходимости погружаться в детали.
Вернёмся к коллайдеру
Конечно же, вы знаете, что ту задачу с суммой диапазона можно решить за O(1) с помощью обычной математической формулы. Лишу вас удовольствия заявить об этом в комментариях.
Вот функция и бенч против предыдущих решений:
public static function sumLinearF(int $a, int $b): int
{
$d = $b - $a + 1;
return (int) (($d - 1) * $d / 2) + $a * $d;
}Summary:
+---+-------------+-------+-------+-------------------+--------+
| # | Name | Iters | Calls | Avg Time | RStDev |
+---+-------------+-------+-------+-------------------+--------+
| 4 | current | 10 | 2000 | 40.102ms | ±1.09% |
| 2 | sumInArray | 10 | 2000 | 12.232ms (-69.5%) | ±0.93% |
| 1 | sumLinear | 10 | 2000 | 77.065µs (-99.8%) | ±3.05% |
+---+-------------+-------+-------+-------------------+--------+Микросекунды вместо миллисекунд. Круто, правда?
И даже здесь есть потенциальные места для оптимизации. Вы, наверное, слышали, что операция деления не всегда самая быстрая.
Деление на 2 можно заменить умножением на 0.5.

public static function multi(int $a, int $b): int
{
$d = $b - $a + 1;
return (int) (($d - 1) * $d * 0.5) + $a * $d;
}+---+---------+-------+---------+--------+------------------+--------+
| # | Name | Iters | Calls | Memory | Avg Time | RStDev |
+---+---------+-------+---------+--------+------------------+--------+
| 1 | current | 10 | 2000000 | 0 | 75.890µs | ±0.79% |
| 2 | multi | 10 | 2000000 | 0 | 78.821µs (+3.9%) | ±0.47% |
+---+---------+-------+---------+--------+------------------+--------+Деление быстрее умножения (╯°□°)╯︵ ┻━━┻
Ожидания не всегда совпадают с реальностью, и оптимизации не всегда работают так, как мы думаем.
А ещё, помня, что вычисления производятся с целыми положительными числами в двоичной системе счисления, мы можем заменить деление на битовый сдвиг, который, в теории, должен отработать ещё быстрее.

public static function shift(int $a, int $b): int
{
$d = $b - $a + 1;
return ((($d - 1) * $d) >> 1) + $a * $d;
}+---+---------+-------+---------+--------+------------------+--------+
| # | Name | Iters | Calls | Memory | Avg Time | RStDev |
+---+---------+-------+---------+--------+------------------+--------+
| 2 | current | 10 | 2000000 | 0 | 75.890µs | ±0.79% |
| 1 | shift | 10 | 2000000 | 0 | 70.559µs (-7.0%) | ±0.70% |
+---+---------+-------+---------+--------+------------------+--------+Ну хоть битовый сдвиг не подвёл.
Обратите внимание, что 7% оптимизации нельзя трактовать так, что битовый сдвиг быстрее именно на 7%. В функции есть ещё несколько математических операций, да и запуск функции занимает какое-то время. Так что 7% — это разница между двумя функциями, а не между двумя конкретными операциями.
💡 Всегда важно понимать, что именно сравнивается, чтобы потом правильно интерпретировать результаты.

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