🌐 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

Performance regression: reverse isSuperTypeOf() guard check in MutatingScope (phpstan-src#5848) costs 13–21% on condition-heavy benches #14920

Description

@ondrejmirtes

Bug report

Performance regression on the 2.2.x dev branch, found by auditing the bench.yml Test job history. Since ad34ca7395 (phpstan/phpstan-src#5848, Skip conditional expression guard when guard type is a subtype of the other branch's value) landed on 2026-06-11, the bench Test job has failed on every single full run (previously it was intermittently green depending on runner speed; the last full green run is 2026-06-10, the first red one is 2026-06-11, and ad34ca7395 is the only src/ commit between them).

The change is 2 lines in MutatingScope (conditional-expression guard selection during branch merging):

$guardIsSuperTypeOfTheirExpr = $guardHolder->getType()->isSuperTypeOf($theirExpressionTypes[$guardExprString]->getType());
+$theirExprIsSuperTypeOfGuard = $theirExpressionTypes[$guardExprString]->getType()->isSuperTypeOf($guardHolder->getType());

 if (
 	$guardIsSuperTypeOfTheirExpr->yes()
+	|| $theirExprIsSuperTypeOfGuard->yes()

The extra reverse isSuperTypeOf() runs per guard holder per branch merge, and in condition-heavy code with big union / constant-array types that is a hot path.

Per-file impact (mode vs. the committed baseline.xml of 2026-05-27, comparable runner classes)

bench baseline tolerance before (Jun 9) after (Jun 11) now (Jul 4)
bug-10538.php 2.61 s ±10 % +11.8 % +25.9 % +33–44 % (further step from phpstan/phpstan-src#5876, tracked separately)
bug-14462.php 158 ms ±25 % +13.1 % +39.2 % +26–49 %
bug-13352.php 3.38 s ±10 % +8.7 % +14.1 % +17–24 % on slower runners
bug-11283.php ~883 ms ±20 % passing +20.3 % fails on slowest runners
bug-7581.php 394 ms ±25 % +30 % intermittent (separate June 9 regression) +73.9 %, permanently red +63–75 %

bug-7581.php had been flirting with the threshold since the June 9 "reasons" batch (tracked separately), but this commit pinned it: 44 consecutive failing runs since, on every runner class.

Local confirmation

In-process bench on current 2.2.x HEAD (median of 5 steady-state iterations, same setup as PHPStan\Benchmark\RegressionBench, PHP 8.5.5), reverting only the 2 lines above:

bench HEAD 2 lines reverted
bug-7581.php 354.3 ms 278.9 ms (−21 %)
bug-10538.php 2 037.5 ms 1 774.5 ms (−13 %)
bug-14462.php 114.9 ms 95.4 ms (−17 %)

There is also an interaction with the later c53a7b8a5f (phpstan/phpstan-src#5876): on bug-14462.php, reverting phpstan/phpstan-src#5876 alone changes nothing, but reverting both recovers −21 % — the two changes compound in the same guard machinery.

Reproduce

# in phpstan-src, 2.2.x
cp tests/bench/data/bug-14462.php test.php
bin/phpstan analyse -l 8 test.php --debug
# or the bench itself:
tests/vendor/bin/phpbench run --file=tests/bench/storage/baseline.xml --report=my-report
tests/bench/data/bug-14462.php
<?php declare(strict_types = 1);

namespace Bug14462;

/** @return array{menu: array<non-empty-string, bool>} */
function get_config(): array {
	return ['menu' => []];
}

$config = get_config();

$data = [ ];
if ($config['menu']['notefrais']) {
	$data[] = [ 'name' => 'notefrais', 'menu' => 'notefrais_base' ];
}
if ($config['menu']['achat']) {
	$data[] = [ 'name' => 'achat', 'menu' => 'achat_base' ];
}

if ($config['menu']['vente-commande_planning'] || $config['menu']['vente-commande']) {
	$data[] = [ 'name' => 'vente' , 'menu' => 'vente_order_recent' ];
}
if ($config['menu']['vente-commande_planning']) {
	$data[] = [ 'name' => 'vente', 'menu' => 'vente_base_planned' ];
}
if ($config['menu']['vente-commande']) {
	$data[] = [ 'name' => 'vente', 'menu' => 'vente_base_com' ];
}
if ($config['menu']['carte']) {
	$data[] = [ 'name' => 'carte', 'menu' => '' ];
}
if ($config['menu']['crm']) {
	$data[] = [ 'name' => 'crm', 'menu' => 'crm_suivi' ];
}
if ($config['menu']['inventaire']) {
	$data[] = [ 'name' => 'inventaire', 'menu' => 'inventaire_base' ];
}


foreach ($data as $row) {
	$stack = [ ];
	if ($row['menu'] === 'vente_order_recent') {
		$stack[] = 'f';
	}
	else {
		$stack[] = 'g';
	}
}

tests/bench/data/bug-10538.php (nested foreach over a ~100-entry constant array of string array shapes) is the other main victim: https://github.com/phpstan/phpstan-src/blob/2.2.x/tests/bench/data/bug-10538.php

Code snippet that reproduces the problem

https://github.com/phpstan/phpstan-src/blob/2.2.x/tests/bench/data/bug-14462.php

Expected output

The false-positive fix from phpstan/phpstan-src#5848 kept, but without the 15–25 % analysis-time cost on condition-heavy code — e.g. by short-circuiting the reverse isSuperTypeOf() check (it is only needed when the forward check didn't already decide), caching it, or deriving the answer from information the merge already has. Bench job green again on typical runners.


Related bench regressions keeping the Test job red since 2026-06-11: #14921 (phpstan-src#5876 — compounds with this change on bug-14462.php), #14918 (type "reasons" commits — the earlier bug-7581.php share), #14919 (ExprHandler dispatch refactor — bug-14207-and.php / and-chain-truthy-blowup.php).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions