Hi everyone

As you probably know, a common performance optimization in PHP is to
prefix global function calls in namespaced code with a `\`. In
namespaced code, relative function calls (meaning, not prefixed with
`\`, not imported and not containing multiple namespace components)
will be looked up in the current namespace before falling back to the
global namespace. Prefixing the function name with `\` disambiguates
the called function by always picking the global function.

Not knowing exactly which function is called at compile time has a
couple of downsides to this:

* It leads to the aforementioned double-lookup.
* It prevents compile-time-evaluation of pure internal functions.
* It prevents compiling to specialized  opcodes for specialized
internal functions (e.g. strlen()).
* It requires branching for frameless functions [1].
* It prevents an optimization that looks up internal functions by
offset rather than by name [2].
* It prevents compiling to more specialized argument sending opcodes
because of unknown by-value/by-reference passing.

All of these are enabled by disambiguating the call. Unfortunately,
prefixing all calls with `\`, or adding a `use function` at the top of
every file is annoying and noisy. We recently got a feature request to
change how functions are looked up [3]. The approach that appears to
cause the smallest backwards incompatibility is to flip the order in
which functions are looked up: Check in global scope first, and only
then in local scope. With this approach, if we can find a global
function at compile-time, we know this is the function that will be
picked at run-time, hence automatically enabling the optimizations
above. I created a PoC implementing this approach [4].

Máté has kindly benchmarked the patch, measuring an improvement of
~3.9% for Laravel, and ~2.1% for Symfony
(https://gist.github.com/kocsismate/75be09bf6011630ebd40a478682d6c17).
This seems quite significant, given that no changes were required in
either of these two codebases.

There are a few noteworthy downsides:

* Unqualified calls to functions in the same namespace would be
slightly slower, because they now involve checking global scope first.
I believe that unqualified, global calls are much more common, so this
change should still result in a net positive. It's also possible to
avoid this cost by adding a `use function` to the top of the file.
* Introducing new functions in the global namespace could cause a BC
break for unqualified calls, if the function happens to have the same
name. This is unfortunate, but likely rare. Since new functions are
only introduced in minor/major versions, this should be manageable,
but must be considered for every PHP upgrade.
* Some mocking libraries (e.g. Symfony's ClockMock [5]) intentionally
declare functions called from some file in the files namespace to
intercept these calls. This use-case would break. That said, it is
somewhat of a fragile approach to begin with, given that it wouldn't
work for fully qualified calls, or unnamespaced code.

I performed a small impact analysis [6]. There are 484 namespaced
functions shadowing global, internal functions in the top 1000
composer packages. However, the vast majority (464) of these functions
come from thecodingmachine/safe, whose entire purpose is offering
safer wrappers around internal functions. Excluding this library,
there are only 20 shadowing functions, which is surprisingly little.
Furthermore, the patch would have no impact on users of
thecodingmachine/safe, only on the library code itself.

As for providing a migration path: One approach might be to introduce
an INI setting that performs the function lookup in both local and
global scope at run-time, and informs the user about the behavioral
change in the future. To mitigate it, an explicit `use function` would
need to be added to the top of the file, or the call would need to be
prefixed with `namespace\`. The impact analysis [6] also provides a
script that looks for shadowing functions in your project. It does not
identify uses of these functions (yet), just their declarations.

Lastly, I've already raised this idea in the PHP Foundations internal
chat but did not receive much positive feedback, mostly due to fear of
the potential BC impact. I'm not particularly convinced this is an
issue, given the impact analysis. Given the surprisingly large
performance benefits, I was inclined to raise it here anyway. It also
sparked some related ideas, like providing modules that lock
namespaces and optimize multiple files as a singular unit. That said,
such approaches would likely be significantly more complex than the
approach proposed here (~30 lines of C code).

Anyway, please let me know about possible concerns, broken use-cases,
or any alternative approaches that may come to mind. I'm looking
forward to your feedback.

Ilija

[1] https://github.com/php/php-src/pull/12461
[2] https://github.com/php/php-src/pull/13634
[3] https://github.com/php/php-src/issues/13632
[4] https://github.com/php/php-src/pull/14529
[5] 
https://github.com/symfony/symfony/blob/7.1/src/Symfony/Bridge/PhpUnit/ClockMock.php
[6] https://gist.github.com/iluuu1994/4b83481baac563f8f0d3204c697c5551

Reply via email to