Содержание

  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», где автор ускоряет свой код многократно, обгоняя Си. Если тема в целом показалась интересной, то вам туда. Удачи.

Комментарии (14)


  1. gro
    05.08.2025 18:47

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


  1. fossfusion
    05.08.2025 18:47

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


  1. bolk
    05.08.2025 18:47

    Интересно, зачем люди вообще пишут substr($var, 0, 1), если можно просто $var[0]?


    1. SavinMikhail
      05.08.2025 18:47

      mb_substr надо


      1. aleksandr-s-zelenin Автор
        05.08.2025 18:47

        Не надо. Надо, только если у вас символы используемые в строках выходят за границы ASCII.


    1. aleksandr-s-zelenin Автор
      05.08.2025 18:47

      Просто не все об этом помнят :)


  1. arokettu
    05.08.2025 18:47

    Для CodeSniffer есть более современный slevomat/coding-standard. У самого CodeSniffer сменился мейнтейнер и разработка пошла более быстро.

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


    1. aleksandr-s-zelenin Автор
      05.08.2025 18:47

      Спасибо за ссылку, интересно, почитаю, отпишусь.


  1. FanatPHP
    05.08.2025 18:47

    Интересно, что практически одновременно с этой статьёй, с разницей в одну минуту, на Реддите был опубликован пост ровно на ту же тему, но с полностью противоположными выводами!
    И приводится условие, при котором разницу (весьма значительную), можно увидеть, и которое не выполнено в приведённом здесь тесте:

    opcache ❌

    Но что ещё интереснее - если здешний тест выполнить при включённом опкеше, то разницы всё равно нет...


    1. FanatPHP
      05.08.2025 18:47

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

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

      return strlen($firstName) + strlen($middleName) +  strlen($lastName);
      

      Даёт мне такой результат

      benchFormatUserName1....................R3 I3 - Mo0.055μs (±1.84%)
      benchFormatUserName2....................R5 I4 - Mo0.036μs (±0.00%)
      


      1. sergeytolkachyov
        05.08.2025 18:47

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

        Спасибо за ссылку. По ней в конце списка функций указано, что он актуален для PHP 8.1. Ссылка на GitHub в качестве источника ведёт в файл zend_compile.c, который уже успел измениться с тех пор, как и список функций PHP, для которых используется специальная обработка. Все эти функции перечислены в zend_try_compile_special_func_ex(). Просто оставлю ссылку посвежее здесь.


    1. aleksandr-s-zelenin Автор
      05.08.2025 18:47

      Спасибо за ссылку, почитаю, может и статью дополню, если будет чем.


  1. SerafimArts
    05.08.2025 18:47

    Давайте не забывать, что:

    1. Проверяем 4 функции, получая +7% с нифига

    • На реальном проекте 100500 вызовов функций, но при сохранении соотношения (в 7%) получим уже другой результат (ну например 0.9с вместо 1с)

    • В случае же циклов - разница может достигать до 20%+ (на практике лексер так ускорил)

    1. В php есть и другие оптимизации (интринсики), например как в случае strlen выше или проверки на тип (is_null, is_int, etc) и прочие.

    • В случае strlen, а особенно если значение константное - \strlen($immutableString), то в коде не будет никаких вызовов функций, а просто прямая замена сразу на вычисленный заранее результат.

    • Если бекслеш отсутсвует, то PHP не может определить является ли эта функция "глобальной" или пользовательской, а следовательно все оптимизации игнорируются.

    1. Что касается других интринсиков, то разница в исполняемом коде может разнИца, например, в 40 раз.

    P.S. Выше в замерах у @FanatPHP отличия в скорости одного из интринсиков почти в ~2 раза, но чисто математически и если отбросить всякие L1/2/3 кеши, предсказания и прочее, как в примере кода ниже, она может отличаться в несколько десятков.

    Пример очень простого кода

    Код

    <?php
    namespace Example;
    
    $a = 23;
    \is_null($a);
    

    Вывод

    JIT$/home/test.php: ; (/home/test.php)
      call 0x55b6ce3cc7e0
      mov $EG(exception), %rax
      cmp $0x0, (%rax)
      jnz JIT$$exception_handler
      jmp ZEND_RETURN_SPEC_CONST_LABEL
    
    А это без бекслеша

    Код

    <?php
    namespace Example;
    
    $a = 23;
    is_null($a); // Просто убрали слеш
    

    Вывод

    JIT$/home/test.php: ; (/home/test.php)
      call 0x556ad05cc7e0
      mov $EG(exception), %rax
      cmp $0x0, (%rax)
      jnz JIT$$exception_handler
      mov 0x40(%r14), %rdx
      mov 0x8(%rdx), %rax
      test %rax, %rax
      jz .L14
    .L1:
      test $0x1, (%rax)
      mov $0x60, %rdi
      jnz .L2
      mov $0x1, %edx
      cmp 0x20(%rax), %edx
      cmova 0x20(%rax), %edx
      sub 0x48(%rax), %edx
      sub 0x40(%rax), %edx
      shl $0x4, %edx
      movsxd %edx, %rdx
      sub %rdx, %rdi
    .L2:
      mov $EG(vm_stack_top), %r15
      mov (%r15), %r15
      mov $EG(vm_stack_end), %rdx
      mov (%rdx), %rdx
      sub %r15, %rdx
      cmp %rdi, %rdx
      jb .L15
      mov $EG(vm_stack_top), %rdx
      add %rdi, (%rdx)
      mov $0x0, 0x28(%r15)
      mov %rax, 0x18(%r15)
    .L3:
      mov $0x0, 0x20(%r15)
      mov $0x1, 0x2c(%r15)
      mov $0x0, 0x30(%r15)
      mov %r15, 0x8(%r14)
      mov 0x18(%r15), %rax
      test $0x300, (%rax)
      jnz .L16
      cmp $0x0, 0x58(%r14)
      jz .L20
      lea 0x50(%r14), %rdi
      cmp $0xa, 0x8(%rdi)
      jnz .L4
      mov (%rdi), %rdi
      add $0x8, %rdi
    .L4:
      mov (%rdi), %rdx
      mov %rdx, 0x50(%r15)
      mov 0x8(%rdi), %eax
      mov %eax, 0x58(%r15)
      test %ah, %ah
      jz .L5
      add $0x1, (%rdx)
    .L5:
      mov $0x556ac08d4770, %rax
      mov %rax, (%r14)
      mov $0x0, 0x8(%r14)
      mov %r14, 0x30(%r15)
      mov 0x18(%r15), %rax
      cmp $0x2, (%rax)
      jnz .L10
      mov $0x0, 0x8(%r15)
      mov $0x0, 0x10(%r15)
      mov 0x38(%rax), %rdx
      test $0x1, %rdx
      jz .L6
      mov $CG(map_ptr_base), %rcx
      add (%rcx), %rdx
      mov (%rdx), %rdx
    .L6:
      mov %rdx, 0x40(%r15)
      mov $EG(current_execute_data), %rcx
      mov %r15, (%rcx)
      mov %r15, %r14
      mov 0x50(%rax), %r15
      mov 0x20(%rax), %edx
      mov 0x2c(%r14), %ecx
      cmp %edx, %ecx
      jg .L21
      test $0x100, 0x4(%rax)
      jnz .L7
      mov %ecx, %edx
      shl $0x5, %rdx
      add %rdx, %r15
    .L7:
      mov 0x48(%rax), %edx
      sub %ecx, %edx
      jle .L9
      shl $0x4, %rcx
      lea 0x50(%r14,%rcx), %rcx
    .L8:
      mov $0x0, 0x8(%rcx)
      sub $0x1, %edx
      lea 0x10(%rcx), %rcx
      jnz .L8
    .L9:
      jmp (%r15)
    .L10:
      test $0x800, 0x4(%rax)
      jnz .L22
    .L11:
      mov $EG(current_execute_data), %rcx
      mov %r15, (%rcx)
      mov %rsp, %rsi
      mov $0x1, 0x8(%rsi)
      mov %r15, %rdi
      call 0x48(%rax)
      mov $EG(current_execute_data), %rax
      mov %r14, (%rax)
      mov %r15, %rdi
      mov $zend_jit_vm_stack_free_args_helper, %rax
      call *%rax
      test $0x4, 0x2a(%r15)
      jnz .L23
      mov $EG(vm_stack_top), %rax
      mov %r15, (%rax)
    .L12:
      test $0x1, 0x9(%rsp)
      jnz .L24
    .L13:
      mov $EG(exception), %rax
      cmp $0x0, (%rax)
      jnz JIT$$icall_throw
      mov $EG(vm_interrupt), %rax
      cmp $0x0, (%rax)
      jnz .L27
      mov $0x556ac08d4790, %r15
      jmp .ENTRY1
    .ENTRY1:
      jmp ZEND_RETURN_SPEC_CONST_LABEL
    .L14:
      mov $0x556ac08d46d0, %rdi
      lea 0x8(%rdx), %rsi
      mov $zend_jit_find_ns_func_helper, %rax
      call *%rax
      test %rax, %rax
      jnz .L1
      mov %r15, (%r14)
      jmp JIT$$undefined_function
    .L15:
      mov %rax, %rsi
      mov $0x556ac08d4730, %rax
      mov %rax, (%r14)
      mov $zend_jit_extend_stack_helper, %rax
      call *%rax
      mov %rax, %r15
      jmp .L3
    .L16:
      cmp $0x0, 0x58(%r14)
      jnz .L17
      mov $0x1, 0x58(%r14)
      jmp .L18
    .L17:
      cmp $0xa, 0x58(%r14)
      jnz .L18
      mov 0x50(%r14), %rcx
      add $0x1, (%rcx)
      mov %rcx, 0x50(%r15)
      mov $0x10a, 0x58(%r15)
      jmp .L19
    .L18:
      call _emalloc_32
      mov $0x2, (%rax)
      mov $0x1a, 0x4(%rax)
      mov $0x0, 0x18(%rax)
      mov 0x50(%r14), %rdx
      mov %rdx, 0x8(%rax)
      mov 0x58(%r14), %ecx
      mov %ecx, 0x10(%rax)
      mov %rax, 0x50(%r14)
      mov $0x10a, 0x58(%r14)
      mov %rax, 0x50(%r15)
      mov $0x10a, 0x58(%r15)
    .L19:
      jmp .L5
    .L20:
      mov $0x556ac08d4750, %rax
      mov %rax, (%r14)
      mov $0x50, %edi
      mov $zend_jit_undefined_op_helper, %rax
      call *%rax
      mov $0x1, 0x58(%r15)
      test %rax, %rax
      jz JIT$$exception_handler
      jmp .L5
    .L21:
      mov $zend_jit_copy_extra_args_helper, %rax
      call *%rax
      mov 0x18(%r14), %rax
      mov 0x2c(%r14), %ecx
      jmp .L7
    .L22:
      mov $zend_jit_deprecated_helper, %rax
      call *%rax
      test %al, %al
      mov 0x18(%r15), %rax
      jnz .L11
      jmp JIT$$exception_handler
    .L23:
      mov %r15, %rdi
      mov $zend_jit_free_call_frame, %rax
      call *%rax
      jmp .L12
    .L24:
      mov (%rsp), %rdi
      sub $0x1, (%rdi)
      jnz .L25
      mov $0x556ac08d4770, %rax
      mov %rax, (%r14)
      call rc_dtor_func
      jmp .L13
    .L25:
      cmp $0xa, 0x8(%rsp)
      jnz .L26
      test $0x2, 0x11(%rdi)
      jz .L13
      mov 0x8(%rdi), %rdi
    .L26:
      test $0xfffffc10, 0x4(%rdi)
      jnz .L13
      call gc_possible_root
      jmp .L13
    .L27:
      mov $0x556ac08d4790, %r15
      jmp JIT$$interrupt_handler
    


  1. ForwardDev
    05.08.2025 18:47

    Все супер! а я бы еще добавил к статьи просто упоминание, когда это не обязательно, чтоб разжевать уже до конца:)

    - В глобальном пространстве имен: Если файл не содержит namespace ...; в начале, то все функции и константы по умолчанию ищутся в глобальном пространстве. strlen() и \strlen() будут работать одинаково.

    - Для функций, импортированных через use function (PHP 5.6+):

    namespace MyApp;
    
    use function \json_encode;
    
    function doSomething() {
        json_encode(...); // Вызов импортированной глобальной функции без \
    }
    namespace MyApp\Utils;
    
    function json_encode($data) {
     // ... кастомная реализация (плохая практика для такого имени, но технически возможно и даже попадался на такое) ...
     }
    
    function process() {
     $data = ['a' => 1];
    
    // Вызов КАСТОМНОЙ функции json_encode из текущего пространства MyApp\Utils:
    $resultCustom = json_encode($data);
    
    // Вызов ВСТРОЕННОЙ PHP функции json_encode (с помощью `\`):
    $resultBuiltin = \json_encode($data);
    
    // ... разные результаты ...
    }

    А самое главное Статические анализаторы (Psalm, PHPStan) - могут предупредить о потенциальных конфликтах имен или неоднозначных вызовах.