Skip to content

Elide provably-passing return type checks at compile time - #23305

Open
staabm wants to merge 1 commit into
php:masterfrom
staabm:elide-return
Open

Elide provably-passing return type checks at compile time#23305
staabm wants to merge 1 commit into
php:masterfrom
staabm:elide-return

Conversation

@staabm

@staabm staabm commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

disclaimer: this change was generated by claude opus. I have little experience with php-src development


Without the opcache optimizer every 'return ;' in a typed function executes ZEND_VERIFY_RETURN_TYPE. Skip emitting it when the check provably always passes:

  • expressions whose opcode always produces a specific type accepted by the return type: comparisons, instanceof, isset/empty and boolean operators produce bool; concatenation produces string;
  • 'return $this->prop;' where the property is declared in the class being compiled and its type is a subtype of the return type. Property types are invariant under inheritance, and typed property reads always yield conforming values: uninitialized reads throw before the return, and __get()/get-hook results are verified against the property type. Only same-class declarations are visible during method compilation, which keeps the proof local.

Closures are excluded (Closure::bind can rebind scopes), as are by-ref returns, generators, union/intersection property types and context-dependent return types (static, callable). Coercion semantics are preserved by requiring the value type set to be a subset of the return type set (e.g. an int property returned as float still emits the check).

Getter-style code drops one opcode per return; 'return $x === null;' style predicates drop the check entirely.


before PR

➜  php-src git:(elide-return) ✗ ./sapi/cli/php_old -n -dopcache.enable_cli=0 return_type_check_elision_repro.php
PHP 8.6.0-dev
iterations=5000000 rounds=7 calls_per_round=40000000

case            best(s)     avg(s)      sink
--------------------------------------------------
getter-direct   0.358463    0.359450    280000000
getter-control  0.409889    0.412215    280000000
cmp-direct      0.462745    0.465084    39999990
cmp-control     0.529274    0.532243    39999990

Direct/control ratio inside each pair (best time, lower is better):
getter           0.8745x
cmp              0.8743x

after PR

➜  php-src git:(elide-return) ✗ ./sapi/cli/php -n -dopcache.enable_cli=0 return_type_check_elision_repro.php
PHP 8.6.0-dev
iterations=5000000 rounds=7 calls_per_round=40000000

case            best(s)     avg(s)      sink
--------------------------------------------------
getter-direct   0.337137    0.337611    280000000
getter-control  0.415257    0.416750    280000000
cmp-direct      0.448156    0.451637    39999990
cmp-control     0.536600    0.539734    39999990

Direct/control ratio inside each pair (best time, lower is better):
getter           0.8119x
cmp              0.8352x

using return_type_check_elision_repro.php

Without the opcache optimizer every 'return <expr>;' in a typed
function executes ZEND_VERIFY_RETURN_TYPE. Skip emitting it when the
check provably always passes:

- expressions whose opcode always produces a specific type accepted by
  the return type: comparisons, instanceof, isset/empty and boolean
  operators produce bool; concatenation produces string;
- 'return $this->prop;' where the property is declared in the class
  being compiled and its type is a subtype of the return type. Property
  types are invariant under inheritance, and typed property reads
  always yield conforming values: uninitialized reads throw before the
  return, and __get()/get-hook results are verified against the
  property type. Only same-class declarations are visible during
  method compilation, which keeps the proof local.

Closures are excluded (Closure::bind can rebind scopes), as are by-ref
returns, generators, union/intersection property types and
context-dependent return types (static, callable). Coercion semantics
are preserved by requiring the value type set to be a subset of the
return type set (e.g. an int property returned as float still emits the
check).

Getter-style code drops one opcode per return; 'return $x === null;'
style predicates drop the check entirely.
staabm referenced this pull request Aug 16, 2026
Without the opcache optimizer every 'return <expr>;' in a typed
function executes ZEND_VERIFY_RETURN_TYPE. Skip emitting it when the
check provably always passes:

- expressions whose opcode always produces a specific type accepted by
  the return type: comparisons, instanceof, isset/empty and boolean
  operators produce bool; concatenation produces string;
- 'return $this->prop;' where the property is declared in the class
  being compiled and its type is a subtype of the return type. Property
  types are invariant under inheritance, and typed property reads
  always yield conforming values: uninitialized reads throw before the
  return, and __get()/get-hook results are verified against the
  property type. Only same-class declarations are visible during
  method compilation, which keeps the proof local.

Closures are excluded (Closure::bind can rebind scopes), as are by-ref
returns, generators, union/intersection property types and
context-dependent return types (static, callable). Coercion semantics
are preserved by requiring the value type set to be a subset of the
return type set (e.g. an int property returned as float still emits the
check).

Getter-style code drops one opcode per return; 'return $x === null;'
style predicates drop the check entirely.
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.

2 participants