[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