Примечание переводчика: я тоже думал, что время статей "Что быстрее — двойные или одинарные кавычки?" прошло еще 10 лет назад. Но вот подобная статья ("What performance tricks actually work") недавно собрала на Реддите относительно большой рейтинг и даже попала в PHP дайджест на Хабре. Соответственно, я решил перевести статью с критическим разбором этих и подобных им "тестов".
Есть множество статей (и даже целых сайтов), посвященных запуску разнообразных тестов, сравнивающих производительность различных синтаксических конструкций, и заявляющих на этом основании что одна быстрее другой.
Главная проблема
Такие тесты являются неверными по многим причинам, начиная с постановки вопроса и заканчивая ошибками реализации. Но что важнее всего — подобные тесты бессмысленны и в то же время вредны.
- Бессмысленны потому, что никакой практической ценности не несут. Ни один реальный проект еще никогда не был ускорен с использованием методов, приводимых в таких статьях. Просто потому, что не различия в синтаксисе имеют значение для производительности, а обработка данных.
- Вредны потому, что они приводят к появлению дичайших суеверий и — что еще хуже — побуждают ничего не подозревающих читателей писать плохой код, думая при этом что "оптимизируют" его.
Этого должно быть достаточно, чтобы закрыть вопрос. Но даже если принять правила игры и притвориться, будто данные "тесты" имеют хоть какой-то смысл, то выяснится, что их результаты сводятся лишь к демонстрации необразованности тестировщика и отсутствия у него всякого опыта.
Одинарные против двойных
Взять пресловутые кавычки, "одинарные против двойных". Разумеется, никакие кавычки не быстрее. Во-первых, есть такая вещь, как opcode cache, которая сохраняет результат парсинга РНР скрипта в кэш-памяти. При этом PHP код сохраняется в формате opcode, где одинаковые строковые литералы сохраняются как абсолютно идентичные сущности, безотносительно к тому, какие кавычки были использованы в РНР скрипте. Что означает отсутствие даже теоретической разницы в производительности.
Но даже если мы не будем использовать opcode cache (хотя должны, если наша задача — реальное увеличение производительности), мы обнаружим, что разница в коде парсинга настолько мала (несколько условных переходов, сравнивающих однобайтные символы, буквально несколько инструкций процессора) что она будет абсолютно необнаружима. Это означает, что любые полученные результаты продемонстрируют лишь проблемы в тестовом окружении. Есть очень подробная статья, Disproving the Single Quotes Performance Myth от core-разработчика PHP Никиты Попова, которая детально разбирает этот вопрос. Тем не менее, почти каждый месяц появляется энергичный тестировщик чтобы раскрыть обществу воображаемую "разницу" в производительности.
Логические нестыковки
Часть тестов вообще бессмысленна, просто с точки зрения постановки вопроса: Например, тест озаглавленный "Действительно ли throw — супер-затратная операция?" это по сути вопрос "Действительно ли, что обрабатывать ошибку будет более затратно, чем не обрабатывать?". Вы это серьезно? Разумеется, добавление в код какой-либо принципиальной функциональности сделает его "медленнее". Но это же не значит, что новую функциональность не нужно добавлять вообще, под столь смехотворным предлогом. Если так рассуждать, то самая быстрая программа — та, которая не делает вообще ничего! Программа должна быть полезной и работать без ошибок в первую очередь. И только после того как это достигнуто, и только в случае, если она работает медленно, ее нужно оптимизировать. Но если сама постановка вопроса не имеет смысла — то зачем вообще тогда тестировать производительность? Забавно, что тестировщику не удалось корректно реализовать даже этот бессмысленный тест, что будет показано в следующим разделе.
Или вот другой пример, тест озаглавленный "Действительно ли $row[id]
будет медленнее, чем $row['id']
?" это по сути вопрос "Какой код быстрее — тот который работает с ошибками, или без?" (поскольку писать id
без кавычек в данном случае — это ошибка уровня E_NOTICE
, и такое написание будет объявлено устаревшим в будущих версиях РНР). WTF? Какой смысл вообще измерять производительность кода с ошибками? Ошибка должна быть исправлена просто потому, что это ошибка, а не потому что заставит код работать медленнее. Забавно, что тестировщику не удалось корректно реализовать даже этот бессмысленный тест, что будет показано в следующим разделе.
Качество тестов
И снова — даже заведомо бесполезный тест должен быть последовательным, непротиворечивым — то есть измерять сравнимые величины. Но, как правило, подобные тесты делаются левой пяткой, и в итоге полученные результаты оказываются бессмысленными и не соответствующими поставленной задаче.
Например, наш бестолковый тестировщик взялся измерять "избыточное использование оператора try..catch
". Но в актуальном тесте он измерял не только try catch
, но и throw
, выбрасывая исключение при каждой итерации цикла. Но такой тест просто некорректен, поскольку в реальной жизни ошибки возникают не при каждом исполнении скрипта.
Разумеется, тесты не должны производиться на бета-версиях РНР и не должны сравнивать мейнстримные решения с экспериментальными. И если тестировщик берется сравнивать "скорость парсинга json и xml", то он не должен использовать в тестах экспериментальную функцию.
Некоторые тесты попросту демонстрируют полное непонимание тестировщиком поставленной им же самим задачи. О подобном примере из недавно опубликованной статьи уже говорилось выше: автор теста попытался узнать, действительно ли код, вызывающий ошибку ("Use of undefined constant") будет медленнее, чем код без ошибок (в котором используется синтаксически корректный строковый литерал), но не справился даже с этим заведомо бессмысленным тестом, сравнивая производительность взятого в кавычки числа с производительностью числа, написанного без кавычек. Разумеется, писать числа без кавычек в РНР можно (в отличие от строк), и в итоге автор тестировал совершенно другую функциональность, получив неверные результаты.
Есть и другие вопросы, которые необходимо принимать во внимание, такие как тестовое окружение. Существуют расширения РНР, такие как XDebug, которые могут оказывать очень большое влияние на результаты тестов. Или уже упоминавшийся opcode cache, который должен быть обязательно включен при тестах производительности, чтобы результаты тестов могли иметь хоть какой-то смысл.
То, как производится тестирование, также имеет значение. Поскольку РНР процесс умирает целиком после каждого запроса, то имеет смысл тестировать производительность всего жизненного цикла, начиная с создания соединения с веб-сервером и заканчивая закрытием этого соединения. Есть утилиты, такие как Apache benchmark или Siege, которые позволяют это делать.
Реальное улучшение производительности
Все это хорошо, но какой вывод должен сделать читатель из данной статьи? Что тесты производительности бесполезны по определению? Разумеется, нет. Но что действительно важно — это причина, по которой они должны запускаться. Тестирование на пустом месте — это пустая трата времени. Всегда должна быть конкретная причина для запуска тестов производительности. И эта причина называется "профилирование". Когда ваше приложение начинает работать медленно, вы должны заняться профилированием, что означает замер скорости работы различных участков кода, чтобы найти самый медленный. После того как такой участок найден, мы должны определить причину. Чаще всего это или гораздо больший, чем требуется, объем обрабатываемых данных, или запрос к внешнему источнику данных. Для первого случая оптимизация будет заключаться в уменьшении количества обрабатываемых данных, а для второго — в кэшировании результатов запроса.
К примеру, с точки зрения производительности нет никакой разницы, используем ли мы явно прописанный цикл, или встроенную функцию РНР для обработки массивов (которая по сути является лишь синтаксическим сахаром). Что действительно важно — это количество данных, которые мы передаем на обработку. В случае, если оно необоснованно велико, мы должны урезать его, или переместить обработку куда-то еще (в базу данных). Это даст нам громадный прирост производительности, который будет реальным. В то время как разница между способами вызова цикла для обработки данных вряд ли будет заметна вообще.
Только после выполнения таких обязательных улучшений производительности, или в случае если мы не можем урезать количество обрабатываемых данных, мы можем приступить к тестам производительности. Но опять же, такие тесты не должны делаться на пустом месте. Для того чтобы начать сравнивать производительность явного цикла и встроенной функции, мы должны быть четко уверены, что именно цикл является причиной проблемы, а не его содержимое (спойлер: разумеется, это содержимое).
Недавний пример из моей практики: в коде был запрос с использованием Doctrine Query Builder, который должен был принимать несколько тысяч параметров. Сам запрос выполняется достаточно быстро, но Doctrine требуется довольно много времени, чтобы переварить несколько тысяч параметров. В итоге запрос был переписан на чистый SQL, а параметры передавались в метод execute() библиотеки PDO, который справляется с таким количеством параметров практически мгновенно.
Значит ли это, что я больше никгда не буду использовать Doctrine Query Builder? Разумеется, нет. Он идеально подходит для 99% задач, и я продолжу использовать его для всех запросов. И только в исключительных случаях стоит использовать менее удобный, но более производительный способ.
Запрос и параметры для этой выборки конструировались в цикле. Если бы у меня возникла дурацкая идея заняться тем, каким образом вызывается цикл, то я попросту потерял бы время без какого-либо положительного результата. И в этом-то заключается самая что ни на есть суть всех оптимизаций производительности: оптимизировать только тот код, который работает медленно в вашем конкретном случае. А не тот код, который считался медленным давным-давно, в далекой-далекой галактике, или код, который кому-то пришло в голову назвать медленным на основании бессмысленных тестов.