Skip to content

Elide the type check on typed property writes when the value type is statically proven#22347

Open
azjezz wants to merge 3 commits into
php:masterfrom
carthage-software:elide-typed-prop-write-check
Open

Elide the type check on typed property writes when the value type is statically proven#22347
azjezz wants to merge 3 commits into
php:masterfrom
carthage-software:elide-typed-prop-write-check

Conversation

@azjezz

@azjezz azjezz commented Jun 16, 2026

Copy link
Copy Markdown

Assigning to a typed property runs zend_assign_to_typed_prop() on every write, a zend_never_inline call that verifies (and may coerce) the value against the property type. When the optimizer can already prove the value satisfies the property type with no coercion, that check is pure overhead.

This is extremely common: constructor property promotion, plain $this->x = $x from typed parameters, and fluent setters all hit it on every call. Neither the DFA optimizer nor the JIT elided it before.

Signed-off-by: azjezz <azjezz@protonmail.com>
@azjezz azjezz force-pushed the elide-typed-prop-write-check branch from 1bbd75c to faa7adc Compare June 16, 2026 22:45
Signed-off-by: azjezz <azjezz@protonmail.com>
Comment on lines 14900 to +14997
@@ -14989,7 +14994,7 @@ static int zend_jit_assign_obj(zend_jit_ctx *jit,
ir_END_list(slow_inputs);
ir_IF_TRUE(if_def);
}
if (ZEND_TYPE_IS_SET(prop_info->type)) {
if (ZEND_TYPE_IS_SET(prop_info->type) && !skip_type_check) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I couldn't verify this locally. and honestly not sure about it..

Signed-off-by: azjezz <azjezz@protonmail.com>
@azjezz azjezz marked this pull request as ready for review June 16, 2026 23:07
@azjezz azjezz requested a review from dstogov as a code owner June 16, 2026 23:07
@TimWolla TimWolla requested a review from iluuu1994 June 17, 2026 06:07
@mvorisek

Copy link
Copy Markdown
Contributor

This PR implements #13100.

I am suprised this PR only improves the performance by 0.04% on Symfony demo. I would expect more performance gain. Does this PR really drops most of the type checks?

@azjezz

azjezz commented Jun 18, 2026

Copy link
Copy Markdown
Author

@mvorisek Sorry did not see the open issue for this!

As for Symfony, honestly no idea, haven't benchmarked it. It does remove type checks for cases that i have tested.

@iluuu1994

Copy link
Copy Markdown
Member

I agree, a statistic of how many type checks in something like Symfony Demo can be elided would be nice. The instruction count difference is effectively nonexistent, so maybe it doesn't work as expected.

@azjezz

azjezz commented Jun 18, 2026

Copy link
Copy Markdown
Author

@iluuu1994 I don't have Symfony Demo set up (and I'm not sure of the right way to wire it up for this), also I'm on macOS, so I can't give you a perf/valgrind hardware instruction count. But I think a direct count of elided checks is actually more to the point than instruction count, so I instrumented for that and ran it on a few real suites.

I put a counter inside zend_assign_to_typed_prop (the runtime typed-property-write check). Baseline binary is the clean pre-feature commit, so it counts every typed-property check executed; the PR binary counts only the checks that remain after elision. The difference is exactly what the optimizer eliminated. Both binaries are otherwise identical, same flags (opcache.optimization_level=-1, opcache on). Counts are independent of JIT (the optimizer flags the same opcodes either way).

Test Total ( baseline ) Remaining Elided %
$this->prop loop, 20M iterations, 2 typed writes 39,999,998 0 39,999,998 100%
PSL terminal suite (501 tests) 39,692 14,293 25,399 64.0%
PSL hpack suite (1183 tests, 102k assertions) 268,982 139,532 129,450 48.1%

So on real test code it elides roughly half to two-thirds of all typed-property type checks. So the pass fires correctly.

Wall time (hyperfine --runs 10 --warmup 2)

Workload Interpreter JIT (tracing)
$this->prop setter loop 497 -> 443 ms (1.12×) 290 -> 206 ms (1.41×)
terminal suite 356 ms (1.01×, noise) 698 ms (1.02×, noise)*
hpack suite 5.06 s (1.00×, noise) 3.67 s (1.01×, noise)

* JIT is a net pessimization on the short terminal run for both binaries (compilation overhead with no hot loops to amortize), so the comparison there isn't that meaningful i suppose.

I think this explains the "instruction count is effectively non existent" observation without it meaning the optimization doesn't work

  • The elision is high, 48-64%, so the pass is doing what it's supposed to.
  • the macro wall-time is near flat because typed prop writes are a small fraction of total work in those benchmarks, hpack for example actually only has 6 property writes, so the majority of 268k writes are from phpunit itself, and wall-time is near flat.
  • the check itself is cheap ( a predicated branch + the zend_assign_to_typed_prop call), so skipping it seems to only show meaningful results in tight loops that do A LOT of property writes, also, the typed-property check goes through the C helper even when JIT-compiled, so this lets the JIT skip the call entirely.

To reproduce: the $this->prop loop is self-contained:

<?php
final class Point {
    public int $x = 0;
    public int $y = 0;
    public function set(int $x, int $y): void { $this->x = $x; $this->y = $y; }
}
$p = new Point();
for ($i = 0; $i < 20_000_000; $i++) { $p->set($i, $i + 1); }

The PSL suites are on the monorepo, cd packages/hpack && composer install && php vendor/bin/phpunit (run per-package, not from the repo root). Happy to run Symfony Demo if someone points me at the right setup, and if anyone on Linux wants a perf stat instruction count I can share the counter patch I used.

Although, I do expect there to be some perf wins in Doctrine during hydration ( and therefore Symfony ).

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.

3 participants