Как стать автором
Поиск
Написать публикацию
Обновить

Нужно ли в PHP перед вызовом функций ставить обратный слэш?

Уровень сложностиСредний
Время на прочтение6 мин
Количество просмотров5.6K

Содержание

  1. Введение

  2. Пространства имён и функции стандартной библиотеки

  3. Опкоды в PHP

  4. Измерение производительности

  5. Заключение

Введение

Несколько лет назад я прочитал статью «How to dump and inspect PHP OPCodes» в которой наконец увидел, что опкоды в PHP действительно существуют. И кроме того, мы, разработчики, которые пишем на PHP на эти опкоды можем влиять, тем самым оптимизируя производительность нашего кода. В статье так же рассказывалось о том, как применение бэкслэшей может ускорить выполнение программ. Я был под впечатлением...

Опкодами (см. Код операции) называется некий промежуточный код, который всё ещё понятен человеку и который выполняется некой исполняющей средой. В случае PHP этой средой является Zend Virtual Machine (она же Zend Engine).

Технология опкодов и виртуальной машины не является чем то уникальным для PHP. Подобный подход использует Java, где опкоды компилируются в их бинарное представление и выполняются Java VM. Исходные коды Java-программ хранятся в файлах с расширением .java, а скомпилированные опкоды в файлах с расширением .class (потому что в Java искодники хранятся только в виде классов). В PHP же исходные коды программ хранятся в файлах с расширением .php. А вот опкоды не хранятся нигде, что вызвало к жизни многочисленные расширения, самым популярным из которых на сегодняшний день является OPcache.

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

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

Пространства имён и функции стандартной библиотеки

В PHP начиная с версии 5.3 появились пространства имён. И с тех пор у нас есть глобальное пространство имён куда входят все дефолтные (или стандартные) функции, классы, константы (и прочая) и пользовательское пространство имён, определяемое при помощи ключевого слова namespace. Обычно, разработчики, не ставят обратный слэш перед вызовами функций, типа:

explode(DIRECTORY_SEPARATOR, '/path/to/file')

Обратите внимание, что константа DIRECTORY_SEPARATOR тоже относится к глобальным константам.

В документации же написано, что если функция вызывается без указания пространства имён, то PHP будет вынужден разрешить (resolve) какую именно функцию нужно вызвать. Этот резолв занимает время и является, обычно, совершенно избыточной процедурой, которую можно было бы легко избежать.

Опкоды в PHP

Для того, чтобы убедиться, что резолв вообще существует нам следует обратиться к опкодам. Для начала давайте напишем немного такого кода в файле test1.php:

<?php

namespace Foo;

function formatUserName(string $firstName, string $middleName, string $lastName) {

    return ucfirst(strtolower($firstName)) . ' ' . ucfirst(substr($middleName, 0, 1)) . '. ' . ucfirst(strtolower($lastName));
}

echo formatUserName('john', 'DanIEl', 'Smith'), PHP_EOL;

Выполним его:

$ php src/test1.php
John D. Smith

И затем посмотрим на опкоды, их тут 44:

$ php -d opcache.enable_cli=1 -d opcache.opt_debug_level=0x20000 src/test1.php 2>&1 > /dev/null

$_main:
     ; (lines=11, args=0, vars=0, tmps=2)
     ; (after optimizer)
     ; /Users/zeleniy/Projects/phpbench/src/test1.php:1-11
0000 EXT_STMT
0001 INIT_FCALL 3 192 string("foo\\formatusername")
0002 SEND_VAL string("john") 1
0003 SEND_VAL string("DanIEl") 2
0004 SEND_VAL string("Smith") 3
0005 V0 = DO_FCALL
0006 ECHO V0
0007 EXT_STMT
0008 T0 = FETCH_CONSTANT (unqualified-in-namespace) string("Foo\\PHP_EOL")
0009 ECHO T0
0010 RETURN int(1)

Foo\formatUserName:
     ; (lines=33, args=3, vars=3, tmps=4)
     ; (after optimizer)
     ; /Users/zeleniy/Projects/phpbench/src/test1.php:5-8
0000 CV0($firstName) = RECV 1
0001 CV1($middleName) = RECV 2
0002 CV2($lastName) = RECV 3
0003 EXT_STMT
0004 INIT_NS_FCALL_BY_NAME 1 string("Foo\\ucfirst")
0005 INIT_NS_FCALL_BY_NAME 1 string("Foo\\strtolower")
0006 SEND_VAR_EX CV0($firstName) 1
0007 V3 = DO_FCALL
0008 SEND_VAR_NO_REF_EX V3 1
0009 V3 = DO_FCALL
0010 T4 = CONCAT V3 string(" ")
0011 INIT_NS_FCALL_BY_NAME 1 string("Foo\\ucfirst")
0012 JMP_FRAMELESS 32 string("foo\\substr") 0019
0013 INIT_NS_FCALL_BY_NAME 3 string("Foo\\substr")
0014 SEND_VAR_EX CV1($middleName) 1
0015 SEND_VAL_EX int(0) 2
0016 SEND_VAL_EX int(1) 3
0017 V3 = DO_FCALL
0018 JMP 0021
0019 V3 = FRAMELESS_ICALL_3(substr) CV1($middleName) int(0)
0020 OP_DATA int(1)
0021 SEND_VAR_NO_REF_EX V3 1
0022 V5 = DO_FCALL
0023 T3 = CONCAT T4 V5
0024 T4 = FAST_CONCAT T3 string(". ")
0025 INIT_NS_FCALL_BY_NAME 1 string("Foo\\ucfirst")
0026 INIT_NS_FCALL_BY_NAME 1 string("Foo\\strtolower")
0027 SEND_VAR_EX CV2($lastName) 1
0028 V3 = DO_FCALL
0029 SEND_VAR_NO_REF_EX V3 1
0030 V5 = DO_FCALL
0031 T3 = CONCAT T4 V5
0032 RETURN T3
LIVE RANGES:
     4: 0011 - 0023 (tmp/var)
     3: 0020 - 0021 (tmp/var)
     4: 0025 - 0031 (tmp/var)

А теперь давайте везде, где можно в коде добавим обратную косую черту и сохраним всё это в test2.php:

<?php

namespace Foo;

function formatUserName(string $firstName, string $middleName, string $lastName) {

    return \ucfirst(\strtolower($firstName)) . ' ' . \ucfirst(\substr($middleName, 0, 1)) . '. ' . \ucfirst(\strtolower($lastName));
}

echo formatUserName('john', 'DanIEl', 'Smith'), \PHP_EOL;

И тоже сгенерим опкодов. Я не буду распечатывать ещё одно полотно неведомых слов, просто скажу, что их получается 36:

$ php -d opcache.enable_cli=1 -d opcache.opt_debug_level=0x20000 src/test2.php

Вот так выглядит diff:

Не помню, где и когда вычитал, но фраза мне тогда понравилась: «самый быстрый код - это код, которого нет». Прекрасно, но что с того? Как это можно выразить/посчитать выгоду? Давайте заюзаем PHPBench о котором я писал в предыдущей статье.

Измерение производительности

Штош, давайте попробуем измерить это так: напишем класс, который будет форматировать имя пользователя двумя способами:

<?php

namespace My\App;

class Backslash {

    public static function formatUserName1(string $firstName, string $middleName, string $lastName) {

        return ucfirst(strtolower($firstName)) . ' ' . ucfirst(substr($middleName, 0, 1)) . '. ' . ucfirst(strtolower($lastName));
    }

    public static function formatUserName2(string $firstName, string $middleName, string $lastName) {

        return \ucfirst(\strtolower($firstName)) . ' ' . \ucfirst(\substr($middleName, 0, 1)) . '. ' . \ucfirst(\strtolower($lastName));
    }
}

А вот наш benchmark-класс:

<?php

namespace My\App\Tests;

use My\App\Backslash;

/**
 * @Revs(1000)
 * @Iterations(5)
 */
class BackslashBench {

    public function benchFormatUserName1() {

        Backslash::formatUserName1('john', 'DanIEl', 'Smith');
    }

    public function benchFormatUserName2() {

        Backslash::formatUserName2('john', 'DanIEl', 'Smith');
    }
}

Запускаем:

$ ./vendor/bin/phpbench run ./tests/Benchmark/BackslashBench.php --retry-threshold=3
PHPBench (1.4.1) running benchmarks... #standwithukraine
with configuration file: /Users/zeleniy/Projects/phpbench/phpbench.json
with PHP version 8.4.8, xdebug ❌, opcache ❌

\My\App\Tests\BackslashBench

    benchFormatUserName1....................R1 I1 - Mo0.576μs (±1.33%)
    benchFormatUserName2....................R1 I0 - Mo0.536μs (±1.20%)

Subjects: 2, Assertions: 0, Failures: 0, Errors: 0

Что же сие означает? Без бэкслешей код выполнился за 0.576μs ± 1.33%, а с бэкслешами за 0.536μs ± 1.20%. Т.к. разброс 1.33 и 1.20 примерно равны, то принебрежём им. Итого получается 0.576 и 0.536. С бэкслешами на 7% быстрее. Но в секундах 0.576 микросекунд - это 0.000000576 секунд. Господи, это сколько? Плакать уже можно?

Не, ну давайте хоть что-то выжмем из этого. С другой стороны разница между 0.576 и 0.536 состовляет не только 0.04 микросекунды, но и ажно 40 наносекунд. Есть такая знаменитая мантра Latency Numbers Every Programmer Should Know и согласно ей получается, что 40 наносекунд - это несколько раз обратиться в ЦПушный кэш уровня L2. Кажется для нас это выхлоп около нуля. Но давайте подумаем, сколько вызовов стандартных функций производит ваш любимый Laravel, Symfony или какой-либо другой фреймворк. Допустим обработка каждого HTTP-запроса делает 100 вызовов. Тогда 40 наносекунд превращается в 4000 наносекунд, а это 4 микросекунды. А если не 100, а 1000? Тогда 40 микросекунд. Давайте будем считать, что HTTP-запрос при хорошем раскладе обрабатывается за 100 милисекунд т.е. за 0.1 секунды (наконец-то хоть одно понятное значение). 40 микросекунд - это 0,04 милисекунды. Нет, ничего нам тут не выжать.

Решение "проблемы"

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

Конечно, вручную дописывать \ к каждой функции никто не захочет, ради такого мизерного выигрыша. Но у нас есть:

  • PHP-CS-Fixer с правилом native_function_invocation, которое автоматически добавит обратный слэш ко всем. Обратите внимание, что правило помечено как risky.

  • PHP_CodeSniffer с плагином soderlind/coding-standard. Но тут проблема в том, что и сам инструмент устарел и плагин не устанавливается, но его можно реанимировать если вдруг вы у себя исторически используете PHP_CodeSniffer.

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

Заключение

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

Всё вышеописанное относится к так называемым микрооптимизациям. И данная статья не является ярким примером того, как можно оптимизировать PHP'ый код продуктивно. Однако, есть и яркие, например, доклад Дмитрия Кириллова «Неочевидные оптимизации опкодов в PHP», где автор ускоряет свой код многократно, обгоняя Си. Если тема в целом показалась интересной, то вам туда. Удачи.

Теги:
Хабы:
+11
Комментарии14

Публикации

Ближайшие события