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

Performance regression: deep !== chains ~30pp slower since TypeSpecifier→ExprHandler dispatch refactor (bench bug-14207-and.php, and-chain-truthy-blowup.php) #14919

Description

@ondrejmirtes

Bug report

Performance regression on the 2.2.x dev branch, found by auditing the bench.yml Test job history: analysing deep !== chains got ~+27–30 percentage points slower on 2026-06-09, affecting two bench files:

  • tests/bench/data/bug-14207-and.php — 90 × 'X' !== $tag_name && … in one expression (currently +53–70 % over the committed baseline of Mo 262 ms, fails the ±25 % tolerance on all but the fastest runner class)
  • tests/bench/data/and-chain-truthy-blowup.phpif with 100 × $x !== "val_n" && … (currently +46–65 % over Mo ≈ 179 ms)

When it started

The regression window is exactly the TypeSpecifier→ExprHandler dispatch batch, merged between the 15:52 and 15:54 bench runs on 2026-06-09:

  • bb18f722eb Dispatch specifyTypesInCondition through ExprHandler::specifyTypes
  • 9ae05a4ded Move single-consumer TypeSpecifier helpers into their handlers
  • 3a70970e7a Extract ConditionalExpressionHolderHelper from TypeSpecifier
  • c128b84527 Extract EqualityTypeSpecifyingHelper from TypeSpecifier

Evidence from the Test job (mode vs. the committed baseline.xml of 2026-05-27):

bench 15:52 run (5997afc9e9) 15:54 run (c128b84527)
bug-14207-and.php +8.68 % +39.38 %
and-chain-truthy-blowup.php +10.93 % +37.87 %
bug-10538.php (control) +11.83 % +12.02 %

The control row shows this is not runner noise: a 3-second array-shape bench on the same two runners stayed flat while both !==-chain benches jumped ~+28 pp. Both files have failed on every non-fastest-class runner since (the 2026-06-10 green run was a fastest-class runner, which masks these two files — that's why the job only went permanently red a day later when phpstan/phpstan-src#5848 landed on top).

Independent from the June 11/June 20 MutatingScope regressions

Local in-process A/B on current 2.2.x HEAD (median of 5 steady-state iterations, PHP 8.5.5): reverting the MutatingScope changes from phpstan/phpstan-src#5848 and phpstan/phpstan-src#5876 — which recovers −19…−21 % on bug-7581.php / bug-10538.php / bug-14462.php — moves these two files not at all:

bench HEAD phpstan/phpstan-src#5848+phpstan/phpstan-src#5876 reverted
bug-14207-and.php 272.6 ms 266.7 ms (−2 %)
and-chain-truthy-blowup.php 177.0 ms 174.4 ms (−1.5 %)

So their entire regression lives in the dispatch batch above. Likely the same root cause as the deep BooleanOr resolveType() slowdown for which 035f580764 added the or-chain-resolve-type.php bench.

Reproduce

# in phpstan-src, 2.2.x
cp tests/bench/data/bug-14207-and.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/and-chain-truthy-blowup.php (abbreviated)
<?php declare(strict_types = 1);

namespace BenchAndChainTruthyBlowup;

/**
 * Regression test for O(N²) in deep BooleanAnd chains.
 * Without the flattening optimization, each level recursed through
 * specifyTypesInCondition and filterByTruthyValue, creating O(N²) scope operations.
 * Slow at the original BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH = 4.
 */
function test(string $x): void {
	if ($x !== "val_1" && $x !== "val_2" && /* … */ && $x !== "val_100") {
		echo $x;
	}
}
tests/bench/data/bug-14207-and.php (abbreviated)
<?php declare(strict_types = 1);

namespace Bug14207And;

use function PHPStan\Testing\assertType;

class HelloWorld
{
	public static function is_not_special(string $tag_name): bool {
		$x = (
			'ADDRESS' !== $tag_name &&
			'APPLET' !== $tag_name &&
			// … 86 more !== comparisons …
			'a9' !== $tag_name
		);

		assertType('bool', $x);
		if ($x) {
			assertType('string', $tag_name);
		}

		return $x;
	}
}

Code snippet that reproduces the problem

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

Expected output

Analysis time of both benches back near the committed CI baseline (Mo 262 ms / Mo ≈ 179 ms on GitHub runners, within the ±25 % tolerance) with no change in inferred types — moving specifyTypesInCondition dispatch into ExprHandlers is a refactor and should be performance-neutral on deep boolean chains.


Related bench regressions keeping the Test job red since 2026-06-11: #14918 (type "reasons" commits, same day), #14920 (phpstan-src#5848 guard check), #14921 (phpstan-src#5876 conditional targets).

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