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.

@iluuu1994 iluuu1994 force-pushed the auto-static-closures branch from a01304f to b4bd2b2 Compare January 2, 2026 00:03
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!

if (name_ast->kind != ZEND_AST_ZVAL || Z_TYPE_P(zend_ast_get_zval(name_ast)) != IS_STRING) {
zend_compile_expr(&name_node, name_ast);
zend_compile_dynamic_call(result, &name_node, args_ast, ast->lineno);
CG(context).closure_may_use_this = true;
Copy link
Member

Choose a reason for hiding this comment

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

Can this one be moved to zend_compile_dynamic_call()?

Copy link
Member Author

Choose a reason for hiding this comment

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

The only other call to zend_compile_dynamic_call() passes compile-time known string names. Do you still think that would be better?

It will be freed by zend_vm_stack_free_args() automatically.
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.

5 participants