Today, a little conversation on Twitter escalated rather quickly. Apparently PHP runs function calls differently depending on namespaced or non namespaced context. When calling functions in a namespaced context, additional actions are triggered in PHP which result in slower execution. In this article, I'll explain what happens and how you can speed up your application.
Wondering how much #PHP I could break by removing the local namespace lookup semantics for namespaced vs. core functions...
— Reviewed, BLYATIFUL! (@Ocramius) December 21, 2016
The conversation started with the tweet above. To understand the difference between global and namespaced function calls better, I'll explain what is going on under the hood.
Calling functions in the global namespace
Function calls in the global namespace look like this:
After parsing this script, the opcodes look like this:
As you can see, this is a simple EXT_FCALL
sequence of opcodes.
Calling functions in a namespace
Function calls in a namespace look like this:
After parsing this script, the opcodes look like this:
The opcodes look pretty much the same as in the global namespace. However, there is one additional opcode added: INIT_NS_FCALL_BY_NAME
. When PHP runs over this opcode, it will check if the function call_user_func()
is found in the namespace. If the function exists in the namespace, PHP will run this one. When the function does not exist in current namespace, PHP will check if it exists in the global namespace and execute that one.
This handy “feature” is frequently (ab)used during testing. A good example for this (ab)use is overwriting the functions to read from or write to the filesystem. In the source files, you can for example use the fopen()
function. During the tests, you can mock this function by placing it in the namespace of the class that you are testing. An example of this can be found in the local adapter of flysystem.
Benchmarks
One of the next tweets stated that a performance gain of 4.5% was made by using fully qualified function calls. Of course, this is just a non-proven number and depends on the project you are working on. To make sure that I am not writing nonsense, I made a little benchmark in PHP 7.1:
Next, I wrote code that can be run with the phpbench tool. There are 4 cases I covered:
- Run a global function in a fully qualified way.
- Run a global function in a non-fully qualified way.
- Run an overridden function that exists globally and in the namespace.
- Run a namespaced function.
I’ve chosen a rather big amount of revs and iterations to make sure the results are accurate. The code in the benchmark looks like this:
This is an overview of the results:
As expected, the fully qualified global function call is the fastest one. This is because PHP does not need to go through the INIT_NS_FCALL_BY_NAME
opcode. When calling the global function in a non-fully qualified way, it is slower. Running functions inside a namespace are always slower then running global functions.
Of course, this is not a big overhead in this simple benchmark. It could be a big overhead if you think about the amount of function calls per run. PHP is not able to optimize this since it is possible that functions get defined during runtime.
Speeding up your application
Currently the only way to speed up the function calls, is by making sure that the global functions are called in a fully qualified way. This is a tedious manual action. You could do this in one of these 2 ways:
Luckily for us, the community is very creative and alert when it comes to performance. Maybe one of the following (future) solutions is less boring to implement:
- Add the php_backslasher package as a git hook.
- Add a
declare(no_dynamic_functions=1)
on top of the PHP file or maybe in a futurenamespace_scoped_declares
method. - Autocompletion to FQ function names in PHPStorm.
- Another awesome package by Ocramius?
- A CLI tool that checks your files for FQ function calls.
- A GrumPHP task.
Conclusion
As you can see, performance killers can hide in small corners. I'm glad to see how this little tweet can get this much feedback from the community in no time. Let's hope that a good solution for this performance problem will be added to PHP soon so that we can easily speed up our applications even more. Now that the secret behind this optimization is revealed, I am looking forward to discover more of these little performance killers.