Как ведущий обратный слеш улучшает производительность вызова функций в PHP

Перевод Why does a backslash prefix improve PHP function call performance, Jeroen Deviaene, 29.08.2023

Вы когда-нибудь замечали, что разработчики добавляют обратный слеш (\) при вызове функций? Многие разработчики (включая меня) так делают, но однако для чего это делается понимают не все. Часто это называют микро-оптимизацией, но каким образом такая простая вещь, как обратный слеш, может улучшить производительность приложения?

Пространства имен в PHP

Пространства имен, добавленные в PHP в версии 5.3, служат для организации классов и функций в приложении. Они похожи на папки в файловой системе, позволяя сгруппировать код и предотвратить конфликт имен. В современном PHP, пространства имен часто отражают файловую структуру приложения.

Для доступа к классу, мы начинаем с корневого пространства имен (\) и затем добавляем родительские пространства имен, разделенные обратным слешем. Например:

$user = new \App\Model\User();

По этому пространству имен, где бы класс не использовался, интерпретатор всегда знает где его найти.

Ведущий обратный слеш

Функциям, также как и классам, можно задать пространство имен. Если мы определим функцию hello() в пространстве имен App\Model, то вызов будет выглядеть так:

\App\Model\hello();

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

Opcode

Opcode – это промежуточный язык, в который PHP интерпретатор преобразует ваш код до выполнения. Изучая этот код, мы можем многое узнать о том, как PHP обрабатывает наш написанный код. Используя 3v4l.org, мы можем конвертировать наш вызов функции в opcode и посмотреть, в чем же отличие вызова функции с обратным слешем и без.

Давайте вызовем какую-нибудь встроенную PHP функцию с обратным слешем, которая находится в определенном пространстве имен.

namespace Foo\Bar;

echo \strtoupper('Hello World');
op          return  operands
---
INIT_FCALL          'strtoupper'
SEND_VAL            'Hello+World'
DO_ICALL    $0
ECHO                $0

Из opcode видно, что мы просто вызываем функцию strtoupper с параметром "Hello World"

Что же будет, если мы уберем ведущий слеш?

namespace Foo\Bar;

echo strtoupper('Hello World');
op                     return  operands
---
INIT_NS_FCALL_BY_NAME          'Foo%5CBar%5Cstrtoupper'
SEND_VAL_EX                    'Hello+World'
DO_FCALL               $0
ECHO                           $0

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

Может возникнуть вопрос: почему интерпретатор не ищет функцию заранее, до создания последовательности команд в opcode? Что ж, это невозможно, так как функции могут быть созданы динамически во время выполнения. Это не позволяет интерпретатору быть уверенным, что будет вызвана нужная функция.

Еще больше оптимизации

Некоторые встроенные функции дают еще большую оптимизацию от добавления обратного слеша. Определенные функции в PHP имеют собственные opcode команды, то есть, если интерпретатор точно знает, что нужно вызвать такую функцию, он заменяет команду вызова функции на специальную команду.

Например, такую оптимизацию имеет функция strlen. Сравните, как отличаются opcode вызова функции с обратным слешем и без.

strlen($a);
op                     return  operands
---
INIT_NS_FCALL_BY_NAME          'Foo%5CBar%5Cstrlen'
SEND_VAR_EX                    !0
DO_FCALL               $1
\strlen($a);
op      return  operands
---
STRLEN  ~1      !0

К таким функциям, имеющим собственные команды, принадлежат count()in_array(), array_key_exists(). Функции сравнения типов, такие как is_null()is_bool()is_string() и подобные. И наконец reflection-функции, такие как get_class()func_get_args() и function_exist()

В заключении

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

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

  1. https://www.php.net/manual/en/language.namespaces.fallback.php ↩︎
  2. https://github.com/squizlabs/PHP_CodeSniffer ↩︎