[wp-trac] [WordPress Trac] #64180: Simplify and improve performance in `WP_Hook::apply_filters()`
WordPress Trac
noreply at wordpress.org
Sat Nov 1 22:14:49 UTC 2025
#64180: Simplify and improve performance in `WP_Hook::apply_filters()`
-------------------------+-----------------------------
Reporter: grapestain | Owner: (none)
Type: enhancement | Status: new
Priority: normal | Milestone: Awaiting Review
Component: Plugins | Version: 6.8.3
Severity: minor | Keywords:
Focuses: performance |
-------------------------+-----------------------------
Hi All!
I am thinking about filters and actions a lot, because it is something
that every site uses and every page load goes trough thousands of, so any
small improvements here can have a multiplied effect globally.
So as I'm looking at code responsible for the execution of filters and
actions I noticed this part of the `WP_Hook::apply_filters()` method:
{{{#!php
<?php
// Avoid the array_slice() if possible.
if ( 0 === $the_['accepted_args'] ) {
$value = call_user_func( $the_['function'] );
} elseif ( $the_['accepted_args'] >= $num_args ) {
$value = call_user_func_array( $the_['function'], $args );
} else {
$value = call_user_func_array( $the_['function'], array_slice(
$args, 0, $the_['accepted_args'] ) );
}
}}}
I guess "Avoid the array_slice() if possible." was added for performance
reasons, although I could not find a specific commit for it as it was
added as part of the class.
I did some tests to see the performance impact of that `array_slice` to
see if it is better to register hooks with all known arguments, even if
those arguments are not used just to avoid the `array_slice` call, or is
it better to use as few arguments as possible.
My tests were not conclusive, probably new versions of PHP have
optimisations in place that make the difference negligible, although it
may have been important at some point.
Anyway, while looking at it I figured that the best way to "Avoid the
array_slice()" is to not use it at all. PHP supports passing more
arguments to functions than what they declare, it is perfectly valid. So
this code in fact works just the same:
{{{#!php
<?php
// Avoid the array_slice() if possible.
f ( 0 === $the_['accepted_args'] ) {
$value = call_user_func( $the_['function'] );
} else {
$value = call_user_func_array( $the_['function'], $args );
}
}}}
Now the question is this: Can the removal of `array_slice()` and the
additional if branch save more CPU than what the passing of the extra
arguments take?
Moreover, passing arguments in case of no arguments accepted by the called
function is also fine, so the code below works also just identical:
{{{#!php
<?php
$value = call_user_func_array( $the_['function'], $args );
}}}
With this one the additional question is if the removal of the branching
saves more than the loss on using the known-to-be-slower
`call_user_func_array()` instead of the `call_user_func()`.
I did tests comparing these cases. In my test case I passed 5 arguments to
`apply_filters()` calls, a random number (to prevent the engine optimising
away the calls), a static number, a static string, the global `$wp_query`
as an example of an object, and a large array with 1000 items, each
consisting of 1000 bytes (1M bytes + overhead).
Here are my results over 1.000.000 `apply_filters` calls:
||Apply Filter call with argument count||Current implementation||No
`array_slice()`||No branching||
||`apply_filter(...,0);`||2.038458s (100%)||2.062812s (101.19%)||2.062165s
(101.16%)||
||`apply_filter(...,1);`||2.129430s (100%)||2.090761s ( 98.18%)||2.085732s
( 97.95%)||
||`apply_filter(...,2);`||2.078453s (100%)||2.085459s (100.34%)||2.049746s
( 98.62%)||
||`apply_filter(...,3);`||2.096780s (100%)||2.097050s (100.01%)||2.047824s
( 97.67%)||
||`apply_filter(...,4);`||2.106713s (100%)||2.088943s ( 99.16%)||2.073354s
( 98.42%)||
||`apply_filter(...,5);`||2.086291s (100%)||2.091264s (100.24%)||2.045195s
( 98.03%)||
As you can see the efforts around avoiding the `array_slice()` are kind of
futile. In some cases it improves a bit, in some cases it is worse, it is
between ~±2%.
But as you can also see the one liner clearly dominates the other two
solutions, except for the 0 argument case. However I'd argue that since
the default value for `$accepted_args` is `1` for both `add_action()` and
`add_filter()`, and also a filter without any argument is probably
extremely rare that first row is most likely an extremely rare occurrence
on any site.
So at the end of the day the rare and little saving that
`call_user_func()` provides is eaten up by the `if` branching and using a
single line of `call_user_func_array()` for all 3 branches differentiated
currently seems to be preferable.
I made a counting on an example site of mine, how many times a
`call_user_func*` is fired in `WP_Hook::apply_filters()`, and it really
seems 0 argument cases are extremely rare:
||Arguments accepted||Admin||Front page||
||0||62||43||
||1||2128||1317||
||2||1164||976||
||3||742||287||
||4||3147||1927||
||5||3||2||
||6+||0||0||
I know one example site is not conclusive at all, but still I think 0
argument calls are extremely rare in the whole ecosystem.
Now I know the differences are small, few percents only, and these are not
at all going to convert to any meaningful speed-up of site loads, because
the time spent by the calling of the hook functions on a real site I
expect to be in the range of 10s of milliseconds (e.g. 7000 hook call *
2us = 14ms), and the savings we can expect is only about 2% of that, that
is a few hundreds of microseconds per page load, but if someone originally
thought it was worth (trying to) optimising the overhead of
`call_user_func_array()` vs `call_user_func()` away, it may still worth to
revisit the issue and switch as it seems on more modern PHP engines this
optimisation no longer valid. (Here I assume it might have been valid in
Sept 2016 (https://github.com/WordPress/wordpress-
develop/commit/61abf68e6d6f7ba496928e70a77a3a7c538ac4f0) when the latest
PHP version was v7.0).
I've done this benchmark on PHP v8.3.12. I'd be surprised if v8.4 or v8.5
would flip the conclusions, but I can imagine that e.g. v7.x could be
different.
--
Ticket URL: <https://core.trac.wordpress.org/ticket/64180>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform
More information about the wp-trac
mailing list