Skip to content

Conversation

@iluuu1994
Copy link
Member

@iluuu1994 iluuu1994 commented Sep 23, 2025

This PR implements static inference for closures, with some restrictions. The closure must not:

  1. Use $this. That's the obvious case.
  2. Use $$var, given $var could be 'this'.
  3. Use Foo::bar(), given this could be a hidden instance call to a (grand-)parent method.
  4. Use $f(), for the same reason as 3.
  5. Use call_user_func(), for the same reason as 3.
  6. Declare another non-static (explicit or inferred) closure, where $this flows from parent to child.
  7. Use require, include or eval, given the called code might do any of the above.

In a Symfony Demo run specifically, static inference works for 68/87 (~78%) closures that were explicitly marked as static by Symfony. That seems quite decent.

The PR also adds caching for static closures that don't have any bindings and don't declare static variables. Instances of such closures can be re-used almost without side-effects (except for object identity).

For Symfony Demo (with static removed from all closures), I measured an improvemend of ~0.1%, so definitely not very significant. Synthetic benchmarks can improve quite a bit, though this will also apply to real-world code that creates closures in loops.

function test() {
    $x = function () {};
}
for ($i = 0; $i < 10_000_000; $i++) {
    test();
}

improves by ~78% in my test runs.

If persistent objects are ever implemented, the instantiation could be done fully at compile-time.

@iluuu1994
Copy link
Member Author

@dktapps Ping. I haven't done any benchmarking yet.

@dktapps
Copy link
Contributor

dktapps commented Sep 23, 2025

This isn't caching static closures yet, right? So there should be no observable effect on performance

@iluuu1994
Copy link
Member Author

iluuu1994 commented Sep 23, 2025

This isn't caching static closures yet, right?

Right. IIRC static closures themselves have a small benefit, though I didn't see it in the CI benchmark. I'm not sure if maybe Symfony already uses linting to add static.

iluuu1994 added a commit to php/benchmarking-symfony-demo-2.2.3 that referenced this pull request Sep 24, 2025
@iluuu1994
Copy link
Member Author

Okay, testing Symfony Demo with all static closures turned into non-static ones shows virtually no improvement. Regardless, they should benefit if we can cache them. I'll try this in the same PR then, given this one isn't useful on its own.

@arnaud-lb
Copy link
Member

One drawback of non-static closures is that they retain $this, which can increase memory usage when the lifetime of $this is supposed to be shorter and references a large graph. I believe this is why coding guidelines and IDEs recommend to manually declare closure as static.

Big +1 on this.

@iluuu1994
Copy link
Member Author

iluuu1994 commented Sep 24, 2025

Quick test: In Symfony Demo with all static closures turned into non-static ones, this patch can turn 122/283 into static ones. That's ~43%, so not bad at all. How many of those can also be cached remains to be seen.

@dktapps
Copy link
Contributor

dktapps commented Sep 27, 2025

Some other weird cases that might need to be considered:

  • Variable variables
  • include,require et al
  • eval()

Source: https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blame/b0a17bf1288a696cba2c8492126f0a485d5637f0/src/Fixer/FunctionNotation/StaticLambdaFixer.php#L110

@dktapps
Copy link
Contributor

dktapps commented Sep 27, 2025

To be honest I start to wonder if this actually makes sense considering the number of obscure conditions that might cause an unintended $this binding just for a potential usage. It'd avoid some unnecessary refs but there'd still be plenty of cases where static would be needed to avoid cycles and accidental refs. (I suppose it'd still benefit for common array_map() style cases where a throwaway closure is used, but 🤷 )

Has there been any discussion about a shorter syntax for static closures? e.g. something like sfn() (idk)

@iluuu1994
Copy link
Member Author

Var var is already handled in this patch. I forgot about eval, but that's quite rare as well, especially in closures.

Copy link
Member

@Girgias Girgias left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack for ext/spl test changes

@dktapps
Copy link
Contributor

dktapps commented Jan 5, 2026

Is there any movement on caching static stateless closures generally? I know the 1cc cache was merged, but there's plenty of cases where explicitly static closures could be cached too without the need for this inference. This inference would just be an added benefit to auto-static stuff in certain cases.

@iluuu1994
Copy link
Member Author

Ofc, explicitly static closures will be cached too, assuming they are stateless.

@dktapps
Copy link
Contributor

dktapps commented Jan 5, 2026

Ok, I hadn't realised caching was also in this PR. Good stuff

Copy link
Member

@arnaud-lb arnaud-lb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks right to me!

@github-actions
Copy link

AWS x86_64 (c7i.24xl)

Attribute Value
Environment aws
Runner host
Instance type c7i.metal-24xl (dedicated)
Architecture x86_64
CPU Intel(R) Xeon(R) Platinum 8488C, 48 cores @ 2400 MHz
CPU settings disabled deeper C-states, disabled turbo boost, disabled hyper-threading
RAM 188 GB
Kernel 6.1.158-178.288.amzn2023.x86_64
OS Amazon Linux 2023.9.20251117
GCC 14.2.1
Time 2026-01-13 15:22:54 UTC

Laravel 12.11.0 demo app - 100 consecutive runs, 50 warmups, 100 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skew P-value Memory
PHP - baseline@8c4f 0.45921 0.46365 0.00054 0.12% 0.45990 0.00% 0.45976 0.00% 3.659 0.999 26.93 MB
PHP - auto-static-closures 0.44028 0.44333 0.00038 0.09% 0.44077 -4.16% 0.44072 -4.14% 3.351 0.000 26.93 MB

Symfony 2.8.0 demo app - 100 consecutive runs, 50 warmups, 100 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skew P-value Memory
PHP - baseline@8c4f 0.77489 0.79602 0.00311 0.40% 0.78241 0.00% 0.78270 0.00% 0.509 0.999 26.96 MB
PHP - auto-static-closures 0.77087 0.78412 0.00246 0.32% 0.77873 -0.47% 0.77924 -0.44% -1.452 0.000 26.96 MB

Wordpress 6.9 main page - 100 consecutive runs, 20 warmups, 20 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skew P-value Memory
PHP - baseline@8c4f 0.66211 0.68058 0.00297 0.45% 0.66357 0.00% 0.66299 0.00% 5.430 0.999 26.98 MB
PHP - auto-static-closures 0.65689 0.65935 0.00042 0.06% 0.65784 -0.86% 0.65778 -0.79% 0.512 0.000 26.96 MB

bench.php - 100 consecutive runs, 10 warmups, 2 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skew P-value Memory
PHP - baseline@8c4f 0.43121 0.43999 0.00163 0.37% 0.43559 0.00% 0.43556 0.00% -0.041 0.999 7.95 MB
PHP - auto-static-closures 0.42158 0.42797 0.00136 0.32% 0.42483 -2.47% 0.42492 -2.44% -0.003 0.000 7.95 MB

?>
--EXPECTF--
Closure [ <user> function {closure:%s:%d} ] {
Closure [ <user> static function {closure:%s:%d} ] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we be printing these as static when they were not created as static? Maybe somehow mark the static as inferred, e.g. <static> instead of static?

Copy link
Member Author

@iluuu1994 iluuu1994 Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No strong feelings, though I think isStatic() should return the inferred value, which may then seem a bit inconsistent. It's also worth noting we're already showing some inferred flags in the same way, e.g. abstract for interface methods. https://3v4l.org/00ieG#v8.5.1

@iluuu1994 iluuu1994 force-pushed the auto-static-closures branch from 165bbb9 to 43dc616 Compare January 19, 2026 13:49
@github-actions
Copy link

AWS x86_64 (c7i.24xl)

Attribute Value
Environment aws
Instance type c7i.metal-24xl (dedicated)
Architecture x86_64
CPU Intel(R) Xeon(R) Platinum 8488C, 48 cores @ 2400 MHz
CPU settings disabled deeper C-states, disabled turbo boost, disabled hyper-threading
RAM 188 GB
Kernel 6.1.158-178.288.amzn2023.x86_64
OS Amazon Linux 2023.9.20251117
GCC 14.2.1
Time 2026-01-19 15:00:50 UTC
Job details https://github.com/php/php-src/actions/runs/21142128198 (Artifacts)
Changeset https://github.com/php/php-src/compare/0caebc..96ca7f

Laravel 12.11.0 demo app - 100 consecutive runs, 50 warmups, 100 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skew P-value Memory
PHP - baseline@0cae 0.45726 0.46371 0.00069 0.15% 0.45797 0.00% 0.45785 0.00% 6.056 0.999 27.03 MB
PHP - auto-static-closures 0.44219 0.44421 0.00043 0.10% 0.44275 -3.32% 0.44265 -3.32% 1.570 0.000 27.03 MB

Symfony 2.8.0 demo app - 100 consecutive runs, 50 warmups, 100 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skew P-value Memory
PHP - baseline@0cae 0.75879 0.77172 0.00220 0.29% 0.76755 0.00% 0.76812 0.00% -1.834 0.999 27.06 MB
PHP - auto-static-closures 0.75747 0.77009 0.00249 0.33% 0.76599 -0.20% 0.76654 -0.21% -1.652 0.000 27.06 MB

Wordpress 6.9 main page - 100 consecutive runs, 20 warmups, 20 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skew P-value Memory
PHP - baseline@0cae 0.66644 0.68717 0.00207 0.31% 0.66804 0.00% 0.66772 0.00% 8.183 0.999 27.06 MB
PHP - auto-static-closures 0.66228 0.68300 0.00216 0.33% 0.66365 -0.66% 0.66326 -0.67% 7.484 0.000 27.06 MB

bench.php - 100 consecutive runs, 10 warmups, 2 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skew P-value Memory
PHP - baseline@0cae 0.42379 0.43149 0.00150 0.35% 0.42667 0.00% 0.42661 0.00% 0.470 0.999 7.94 MB
PHP - auto-static-closures 0.43164 0.43861 0.00134 0.31% 0.43417 1.76% 0.43395 1.72% 0.531 0.000 7.95 MB

@github-actions
Copy link

AWS x86_64 (c7i.24xl)

Attribute Value
Environment aws
Instance type c7i.metal-24xl (dedicated)
Architecture x86_64
CPU Intel(R) Xeon(R) Platinum 8488C, 48 cores @ 2400 MHz
CPU settings disabled deeper C-states, disabled turbo boost, disabled hyper-threading
RAM 188 GB
Kernel 6.1.158-178.288.amzn2023.x86_64
OS Amazon Linux 2023.9.20251117
GCC 14.2.1
Time 2026-01-19 23:24:29 UTC
Job details https://github.com/php/php-src/actions/runs/21154141728 (Artifacts)
Changeset https://github.com/php/php-src/compare/0caebcd196..96ca7fa073

Laravel 12.11.0 demo app - 100 consecutive runs, 50 warmups, 100 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skew P-value Memory
PHP - baseline@0caebcd 0.44661 0.45030 0.00044 0.10% 0.44720 0.00% 0.44713 0.00% 3.736 0.999 27.00 MB
PHP - auto-static-closures 0.43096 0.43464 0.00046 0.11% 0.43155 -3.50% 0.43145 -3.51% 3.433 0.000 27.00 MB

Symfony 2.8.0 demo app - 100 consecutive runs, 50 warmups, 100 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skew P-value Memory
PHP - baseline@0caebcd 0.76308 0.77568 0.00265 0.34% 0.77247 0.00% 0.77328 0.00% -1.988 0.999 27.01 MB
PHP - auto-static-closures 0.76260 0.77571 0.00284 0.37% 0.77207 -0.05% 0.77291 -0.05% -1.870 0.008 27.01 MB

Wordpress 6.9 main page - 100 consecutive runs, 20 warmups, 20 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skew P-value Memory
PHP - baseline@0caebcd 0.66457 0.66720 0.00042 0.06% 0.66540 0.00% 0.66534 0.00% 1.027 0.999 27.01 MB
PHP - auto-static-closures 0.66029 0.68065 0.00302 0.46% 0.66168 -0.56% 0.66114 -0.63% 5.744 0.000 27.01 MB

bench.php - 100 consecutive runs, 10 warmups, 2 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skew P-value Memory
PHP - baseline@0caebcd 0.42382 0.43063 0.00135 0.32% 0.42676 0.00% 0.42671 0.00% 0.064 0.999 27.01 MB
PHP - auto-static-closures 0.43190 0.45228 0.00284 0.65% 0.43486 1.90% 0.43450 1.83% 4.165 0.000 27.01 MB

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants