Как стать автором
Обновить

Комментарии 39

Простой и полезный скрипт, спасибо, часто провожу подобные тесты на коленке, взял на вооружение.

Столкнулся с округлением на маленьких значениях, попробую предложить pr, возможно с show($precision) , округляя уже на этапе вывода статистики.

    $range = [0, 1000000]; // 1M

    (new Comparator())
        ->iterations(100000) // 100K
        ->withoutData()
        ->compare([
            'rand' => fn() => rand(...$range),
            'mt_rand' => fn() => mt_rand(...$range),
            'random_int' => fn() => random_int(...$range),
        ]);

Да, я тоже понял это и добавил возможность вручную задавать округление. Статью обновил. Спасибо за отзыв :)

$range = [0, 1000000];

(new Comparator())
    ->iterations(100000)
    ->withoutData()
    ->compare([
        'rand'       => fn () => rand(...$range),
        'mt_rand'    => fn () => mt_rand(...$range),
        'random_int' => fn () => random_int(...$range),
    ]);
 ----- --------------------- -------------------- --------------------
  #     rand                  mt_rand              random_int
 ----- --------------------- -------------------- --------------------
  min   0                     0                    0
  max   0.00011301040649414   3.8862228393555E-5   3.6001205444336E-5
  avg   9.6567630767822E-7    9.7702264785767E-7   1.0187935829163E-6
 ----- --------------------- -------------------- --------------------
        winner                loser                loser
 ----- --------------------- -------------------- --------------------

А как там с мультимодальными распределениями дела?

Пакету не важно что отправляется в колбэк. По сути, весь код это обёртка над:

$startAt = microtime(true);

/* user function */
$callback();

return microtime(true) - $startAt;

То есть бесполезно.

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

Долго корпели, считали, и получили "виннера" с разницей меньше погрешности измерения.
А потом и рождаются все эти адские байки про "одинарные ковычки быстрее"


и можно не тратить время на реализацию данного функционала

Да-да, и потратить его на бессмысленные измерения.

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

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

Странно, что вы подхватили этот, в сущности, второстепенный вопрос с кавычками. Речь не о нем. Но, с другой стороны, это как раз очень характерно для таких увлеченных оптимизаторов, которым надо "здесь и сейчас", без всякой связи с реальностью и без попытки подумать хотя бы на один ход вперёд. И в этом смысле кавычки очень показательны, да.


Логично предположить, что если нас интересует производительность РНР кода, то первым шагом, который будет сделан — это включение опкод кэша. В котором, как можно увидеть, уже и близко не остается никакой разницы между кавычками. То есть, если нас интересует реальная реальная производительность, а не воображаемая, то она достигается не разницей между кавычками.


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


А все это крохоборство на синтаксисе — это самообман, и пустая трата ресурсов.

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

Согласен, показательны. Показательны в том, что, во-первых, не нужно использовать "расширенную" механику там где она не оправдана, а также в том, что предварительная оптимизация является плохим решением.

(new Comparator())
    ->withoutData()
    ->iterations(100000)
    ->compare([
        'single' => fn () => 'foo' . $value,
        'double' => fn () => "foo$value",
    ]);
Результаты выполнения
 ----- -------------------- ---------------------
  #     single               double
 ----- -------------------- ---------------------
  min   0                    0
  max   5.3167343139648E-5   0.00011587142944336
  avg   8.6287260055542E-7   8.7460517883301E-7
 ----- -------------------- ---------------------
        winner               loser
 ----- -------------------- ---------------------
 ----- -------------------- --------------------
  #     single               double
 ----- -------------------- --------------------
  min   0                    0
  max   2.1934509277344E-5   2.598762512207E-5
  avg   8.3098649978638E-7   8.3781242370605E-7
 ----- -------------------- --------------------
        winner               loser
 ----- -------------------- --------------------
 ----- -------------------- --------------------
  #     single               double
 ----- -------------------- --------------------
  min   0                    0
  max   2.7894973754883E-5   4.1961669921875E-5
  avg   8.3756923675537E-7   8.5575819015503E-7
 ----- -------------------- --------------------
        winner               loser
 ----- -------------------- --------------------

Если код работает медленно, надо не кавычки переставлять, а подход менять.

Именно для этого и нужен этот пакет - понять какой подход будет работать быстрее.

А все это крохоборство на синтаксисе — это самообман, и пустая трата ресурсов.

Понял, Ваш тимлид не даёт пачкать код и Вы таким образом пытаетесь вылить негатив на меня. Ок, учту.

Но вы-то в ваших тестах меняете не подход, а синтаксис. При том что для радикальных изменений какие-то особые измерения не нужны — при нормальной оптимизации все будет видно невооруженным взглядом. Скажем, вместо того, чтобы читать значение из гигабайтного джейсон файла, использовать хранилище с произвольным доступом. А ради разницы в 0.2 миллисекунды и затеваться не стоило.


Понял, Ваш тимлид не даёт пачкать код

На будущее, делать предположения о событиях, не относящихся к обсуждаемому вопросу — это не очень хорошая практика, и не очень одобряется сообществом.

Ради 0.2 мс разумеется не стоит. Это экономия на спичках получится. В этом случае лучше выбрать путь, позволяющий легче реализовывать задачу.

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

Сам кейс заключался в проверки двух подходов: на 1000 записей выполнить 1 джобу, которая одним запросом получит из базы все значения, произведёт вычисления и вторым запросом массово положит обратно, или же выполнить 1000 отдельных джоб, выполняющих одну операцию.

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

Тем не менее, вопрос был не в целостности и актуальности данных, а именно в скорости работы двух подходов.

...не относящихся к обсуждаемому вопросу...

Обратите внимание, что данный вопрос как раз-таки обсуждался и начали это именно Вы.

Ну то есть вам понадобился тест, чтобы выяснить, что один сделать запрос будет быстрее, чем последовательно выполнить 1000. Понятно.

На практике далеко не всегда 1 запрос будет работать быстрее 1000 отдельных. Так что да, для проверки потребовалось тестирование.

да, но что мешало сделать shared lock в транзакции? также можно применить и атомарные операции, да много чего можно придумать не используя 1000 апдейтов

О, да вы обновили комментарий, добавив в него пример, на котором я как раз хотел остановиться, но не было подходящего кейса. Спасибо, это ровно то, чего здесь не хватало!


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


На самом деле главной проблемой всех этих тестов является даже не их бессмысленность, а то, что тестирующий зачастую не понимает — а что он, собственно, тестирует-то. Но ничтоже сумняшеся делает далеко идущие выводы. Продумать эксперимент и правильно интерпретировать его результаты — это целая наука. Про это даже хороший анекдот есть:


Один исследователь решил узнать, где у таракана уши.
Поймал таракана, посадил на стол, постучал по столешнице — таракан убежал.
Поймал снова, оторвал ноги, постучал — таракан никуда не бежит. Значит, не слышит.
Вывод — у таракана уши в ногах!

Вот и сейчас.
Вы зачем-то добавили в свой комментарий тест "конкатенация vs. интерполяция". Но ведь хотели-то — "single vs. double", верно?


Здесь у вас получилось как минимум две методологические ошибки:


  • во-первых, вы вообще никак не тестируете парсинг строк. Парсится этот ваш код "внутренним компилятором" ровно один раз, при старте. А когда запускается цикл на офигиллиард итераций, то оба строковых литерала — это уже просто адреса в памяти, без всяких кавычек. И внутри цикла никакого парсинга не происходит. Чтобы протестировать разницу в парсинге, вам надо запускать в цикле именно парсинг. Инклюдом например (убедившись в том, что опкод кэш отключен). Но в этом случае у вас погрешность будет вносить дисковый ввод-вывод.
  • во-вторых, как я написал выше, вся разница в вашем коде заключается в способе объединения двух строк, конкатенация vs. интерполяция. Совершенно лишняя операция, которую вы зачем-то добавили. Если бы кавычки в этом тесте парсились, то добавление лишней операции исказило бы результаты. Но поскольку единственным отличием двух тестов является способ соединения строк, то в итоге вы тестируете вообще не то, что хотели

И это мы еще даже не касались размеров или количества операндов. А на результаты теста может влиять что угодно — и состав операндов стоит одним из первых в очереди и смотрит на вас грустными глазами Шлёмы-маляра. И стоит нам поменять тестовую строку с "Hallo $world"; на "Hi! My name is $name and I am $age years old! I love doing $hobby!"; как виннеры и лузеры внезапно меняются местами! И что теперь делать с этими результатами? Продолжать рассказывать всем, что одинарные быстрее?


Вот это-то и является главной проблемой. Берется какой-нибудь вырожденный случай, который в лучшем случае имеет весьма отдаленное отношение к исследуемой проблеме, а обычно — так и вообще никакого. И потом на основании полученных результатов переписываются гайдлайны, рефакторится значительное количество кода, распространяются нелепые слухи. А всего этого вполне можно было бы избежать, если не упарываться в микротесты, а заниматься оптимизацией профессионально — отталкиваясь от результатов профайлинга...

В целом, согласен. Тестирование тоже своего рода искусство.

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

Не совсем. Выражения:


echo 'Hello World';
// vs
echo "Hello World";

Ну вообще ничем не отличаются и опкод у них полностью идентичный.


Так что про "какие кавчки быстрее" — это миф от людей, которые не смогли вместо php test.php написать php -dopcache.opt_debug_level=0x20000 test.php.

Идея лучше реализации. Нужно считать стандартное отклонение и сравнивать уже учитывая его. Так же имеет смысл указывать место на пьедестале а не победитель и остальные.

ознакомьтесь с google benchmark - идея та же но реализацию они отточили чтобы этим цифрам можно было верить

Ну как сказать лучше... Так или иначе самый наипростейший способ отслеживать время выполнения кода - использовать функцию microtime сравнивая значения до и после. Пример писал в комментарии, поэтому дублировать не стоит. И именно "стандартное отклонение", читай "среднее арифметическое" и берётся как основное значение для сравнения, т.к. минимальная и максимальная пиковая нагрузка не всегда может показывать реальные показатели по каким-либо причинам.

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

По поводу Google Benchmark, он под C++ и у него свои особенности реализации. В случае с PHP "бенчмарк" совсем по-другому работает, но общий принцип тот же - замер времени между началом и завершением выполнения. И эта цель достигнута в этом программном продукте.

Бенчмарк - это не про прогнать несколько раз и посчитать, это ещё и про оценку того, насколько статистике можно верить, на основе статистики.
Вы ведь не станете утверждать, что распределение времени всегда строго нормальное?

Это решение не про бенчмарки, на сколько я понял автора, а про "сравнить мои решения между собой" по времени на глазок и выявить явного лидера. Даже "winner/loser" как бы намекают на несерьёзность :)

Если бы мне нужно было сравнение в изолированной среде с нормальным распределением, замерами памяти, сети и io, я бы выбрал другое решение, и потратил бы на него больше времени.

Аналогично. Если нужно что-то более мощное, то явно воспользовался другими инструментами тестирования. Да хоть тот же Яндекс.Танк для нагрузочного тестирования.

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

Нет, не стану, и именно поэтому основной единицей сравнения является среднее арифметическое из всех значений, т.к. оно ближе к правде, но не всегда соответствует. И это понимает любой разработчик.

И бенчмарк определяет производительность, а это не про данное программное решение. Его задача определить скорость выполнения участка кода с ограниченной областью видимости. Бенчмарком здесь и не пахнет. Мало того, он и не претендует на него. Вы видите слово "benchmark" в описании? Вот именно.

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

Как уже писал раннее, Runtime Comparison не претендует на точный инструмент.

А потом в комментах вы сравниваете своим инструментом двойные кавычки с одинарными? Вы тролите что-ли?

Наипростейший != правильный

Начнём с microtime - какая у него точность? Идём в документацию и видим "For performance measurements, using hrtime() is recommended.", упс...

Дальше лучше, смотрим на данные из параграфа "Округление значений"
Казалось бы всё однозначно - минимум, максимум и среднее варианта А меньше варианта Б, однако проведём Т-тест

> a = c(0.0112, 0.0147, 0.0153, .0157, .0154)
> b = c(.015, .0155, .0153,.015, .0158)
> summary(a)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
0.01120 0.01470 0.01530 0.01446 0.01540 0.01570 
> summary(b)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
0.01500 0.01500 0.01530 0.01532 0.01550 0.01580 
> sd(a)
[1] 0.001858225
> sd(b)
[1] 0.0003420526
> t.test(a, b)

        Welch Two Sample t-test

data:  a and b
t = -1.0178, df = 4.2708, p-value = 0.3629
alternative hypothesis: true difference in means is not equal to 0
95 percent confidence interval:
 -0.003148545  0.001428545
sample estimates:
mean of x mean of y 
  0.01446   0.01532 

P-value говорит что основная гипотеза (распределения равны) верна, т.е. на самом деле там одинаковые распределения.

google benchmark я привёл как инструмент который делает бенчмарки правильно - там много нюансов которые увеличивают точность измерений:

  • возможность задать код до и после измерений, чтобы подготовить данные и почистить за собой

  • автоматический подбор количества итераций — проще для пользователя и позволяет реагировать на случайные выбросы в статистике

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

  • авто подсчёт нужных статистик

  • исключение выбросов, к примеру если ОС тупанула во время измерений

Runtime Comparison не претендует на звание бенчмарка, а для проверки скорости участков кода microtime хватает за глаза.

имеет смысл сделать, откидывание крайних значений, или брать например, 30%(задаваемо) лучших значений

Задаваемые значения точно не подойдут, т.к. пакет заранее не знает что именно передаст ему разработчик и в каком виде. Что касается крайних значений, именно поэтому и используется среднее арифметическое, что позволяет получать приблизительное время выполнения без плясок с бубном.

имелось ввиду другое, у тебя например есть 100 прогонов, и ты можешь сказать - учесть только 30 из них лучших, т.к. у тебя также какие то процессы на компе могут испортить все показатели, а лучшие значение не изменяться

Да, верно. Этот же пакет фреймворко-независимый.

Кстати, бенчмарк в Laravel выводит лишь время.

Например:

$range = [0, 1000000];

Benchmark::dd([
    'rand' => fn () => rand(...$range),
    'mt_rand' => fn () => mt_rand(...$range),
    'random_int' => fn () => random_int(...$range),
], 100);
array:3 [
  "rand" => "0.001ms"
  "mt_rand" => "0.002ms"
  "random_int" => "0.001ms"
]

Если этого достаточно, то вполне годный инструмент.

Под капотом та же самая функция hrtime с последующим расчётом среднего значения из списка результатов.

У вас идет измерение времени выполнения, а как насчет используемой памяти?

Интересная идея. Добавлю в ближайшее время. Спасиб

Версия 2.1.0 релизнута.

Единственное, функция memory_get_usage работает нестабильно - для итерации показывает 0 байт затраченной памяти, а при отслеживании памяти всех итераций, значение есть.

Например:

 ------- ----------------------- ------------------------ ------------------------
  #       foreach                 array_map                array_walk
 ------- ----------------------- ------------------------ ------------------------
  min     0.8432 ms - 0b          1.0902 ms - 0b           1.1327 ms - 0b
  max     2.2471 ms - 0b          2.3505 ms - 0b           2.6106 ms - 0b
  avg     0.953616 ms - 0b        1.249029 ms - 0b         1.257498 ms - 0b
  total   952.5027 ms - 417.4Kb   1244.7641 ms - 412.9Kb   1260.8613 ms - 412.9Kb
 ------- ----------------------- ------------------------ ------------------------
  Order   - 1 -                   - 2 -                    - 3 -
 ------- ----------------------- ------------------------ ------------------------

Дело в том, что memory_get_usage показывает объём используемой памяти в текущий момент. У нас тестируемым является колбэк, который запускается, использует какое-то количество памяти, потом завершается, и в этот момент объём памяти высвобождается, поэтому в результате колбэка мы видим ноль.

Вариантом является тестирование через memory_get_peak_usage, так как тут мы увидим не текущее, а максимальное пиковое потребление.

Мои изыскания можно посмотреть тут:
https://github.com/dekmabot/runtime-comparison/blob/test-memory/src/Metrics/Memory.php

Остановился в решении на том, что нужно сбрасывать максимальное потребление между вызовами. И такой метод есть: memory_reset_peak_usage, но он появляется только в php8.2, поэтому внедрять его сейчас в библиотеку наверное рановато. А другого способа сбрасывать я не нашёл.

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

Также, если я правильно заметил, phpbench сравнивает предыдущее и текущее время обработки метода, а пакет из статьи разные варианты между собой.

В конце статьи есть спойлер с кодом, можете сравнить с приведённым Вами.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории