Skip to content

Subtract a union from plain mixed at once and build comparison forms once per node - #6972

Closed
ondrejmirtes wants to merge 3 commits into
2.3.xfrom
faster-comparison-narrowing
Closed

ondrejmirtes wants to merge 3 commits into
2.3.xfrom
faster-comparison-narrowing

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

This makes the analysis of tests/bench/data/bug-6265.php, bug-12159.php and bug-14869.php cheaper again after two recent commits made them slower. Inferred types and reported errors are unchanged.

Cause

  • eea09a9 "Narrow floats into ranges in comparisons, …": the comparison types (getSmallerType(), getGreaterOrEqualType(), … on IntegerRangeType and ConstantNumericComparisonTypeTrait, and the false branch in BinaryOpHandler) build TypeCombinator::remove(new MixedType(), <union>), and their unions now also hold NAN and a FloatRangeType. TypeCombinator::doRemove() removed such a union from mixed one member at a time, and every step went through MixedType::subtract(), which rebuilt the growing subtracted union with TypeCombinator::union(). In an xdebug profile of bug-12159, remove() went from 57 M to 134 M units.
  • 4dab534 "Read remembered equivalent and complementary comparison forms …" (Read remembered equivalent and complementary comparison forms when resolving ===, !==, ==, !=, <, <=, >, >= #6948): every ===, !==, ==, !=, <, <=, > and >= built 2–6 new form nodes twice, once when resolving its type and once in specifyTypes. Each lookup with hasExpressionType() printed these nodes through the pretty printer, because new nodes have no cached print.

Changes

Each PHP change is mirrored in its turbo-ext C++ twin in the same commit.

  • Subtract a union from plain mixed at once (TypeCombinator::doRemove()): when the type is exactly MixedType with no subtracted type and the removed type is exactly a UnionType, the result is $fromType->subtract($union), built once. The condition is deliberately narrow. Letting every mixed remove a union at once changed a type description in nsrt/mixed-subtract.php (array<mixed, mixed> became array<mixed>). With the narrow condition, a temporary check over the whole test suite compared the result with the member-by-member result (equals() plus precise() and cache() descriptions) and found no difference.
  • Build the equivalent comparison forms once per comparison node (BinaryOpHandler): the forms and their printed keys are kept in a phpstan_comparison_forms attribute on the comparison node. The cache is checked against the node's class and its left and right operands, because attributes are copied to new nodes. The scope holders are then read by key from MutatingScope::$expressionTypes with the same certainty-yes check. The "integer operands" guard on the complement forms is unchanged.
  • Keep the comparison forms with their complements in the cache: a follow-up that stores the forms with their complements appended, so a lookup returns a cached list instead of merging two.

Overlap with another change

5247de4 ("Subtract a union from plain mixed at once") overlaps with 762a3d716 "Remove a union from mixed by subtracting its members at once" (f23d802 after its rebase, branch bug-15061-isset-fold, Amp thread T-01a1b134-ed02-7078-a72e-a1e527d957a3). Both change how TypeCombinator::doRemove() removes a union from mixed. Whichever lands second gets rebased onto the other.

Measurements

Net instruction counts: cachegrind over one BenchCase::runAnalyse() of the file, minus the same for an empty file, after two warm-up runs. This is the PHP implementation with OPcache off. Base is origin/2.3.x bcca2b0; the branch is 2c7e7f7.

file 2.3.x this PR change
bug-6265.php 1.3450 G 1.1951 G −11.1 %
bug-12159.php 3.3575 G 3.1497 G −6.2 %
bug-14869.php 2.2765 G 2.2139 G −2.7 %

Wall clock: bin/phpstan analyse -c <config> --debug --no-progress -q tests/bench/data/<file>. tests/bench/phpstan.neon does not exist, so the config was a temporary one mirroring BenchCase: conf/bleedingEdge.neon plus level: max, with a separate tmpDir per side. The result cache was cleared before every run. There were 28 base/branch pairs, run alternately, and the first 3 pairs were dropped as warm-up, so each median is over 25 runs per side. No turbo extension was loaded. The machine is an Amp orb (Intel Xeon @ 2.60 GHz, 16 vCPUs) running PHP 8.4.27 NTS, with OPcache enabled and JIT disabled by PHPStan's restart. The times were taken on b3adc67, before the follow-up commit.

file 2.3.x median (IQR) this PR median (IQR) median of paired differences
empty file 0.422 s (0.396–0.449) 0.407 s (0.395–0.448) −0.017 s
bug-6265.php 0.855 s (0.820–0.938) 0.860 s (0.801–0.908) −0.017 s
bug-12159.php 0.924 s (0.884–0.963) 0.899 s (0.848–0.965) −0.026 s
bug-14869.php 0.759 s (0.732–0.807) 0.757 s (0.743–0.790) +0.004 s

On this machine the wall-clock difference is within noise: the paired differences of the three files are about the same as the −17 ms of the empty file, and the run-to-run spread is around ±50 ms. Counting instructions under bin/phpstan (OPcache on, children traced) also gave a drop of the same size as the table above. That measurement varies by about 0.1 G between runs, because the CLI does timing-dependent work, so it is not reported here.

Tests

  • make tests (paratest, 23472 tests) passes without the turbo extension, and with it loaded through PHP_INI_SCAN_DIR. Reflection confirmed the native TypeCombinator::doRemove() and BinaryOpHandler::createComparisonForms() were active.
  • turbo-ext: strict build (make WARN_FLAGS="$(make -s print-warn-flags)"), tests/smoke.php (ALL OK), tests/signature-parity.php (OK), and tests/walk-trace.php (identical, 700329 trace lines). bin/side-by-side.php reports nothing for the changed classes. On this PHP 8.4 build it reports the same 59 "stale" generated headers on clean 2.3.x as on this branch, which come from rendering self.
  • make cs (phpcs) passes, and bin/phpstan analyse -c build/phpstan.neon on the two changed PHP files is clean.
  • The three benchmark files report identical errors on 2.3.x and on this branch.

Removing a union from mixed peeled its members one at a time, and every
step rebuilt the growing subtracted union. Plain mixed minus a union is
mixed with that union subtracted, so subtract it in one step. The
comparison types (getSmallerType() and friends) build exactly that, and
since floats narrow into ranges their subtracted unions carry NAN and a
float range as well.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1b134-f541-75cc-9cd3-8f0206f0ec3f
Resolving a comparison and specifying its types each built the
equivalent and complementary forms again and printed every one of them
to look it up in the scope. Keep the forms with their printed keys in
the comparison node's attributes and read the scope's holders by key.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1b134-f541-75cc-9cd3-8f0206f0ec3f
Merging the cached equivalent and complement forms on every lookup
built a new array each time, and reading them from the node attribute
left array_merge() without a list type. Store the forms followed by
their complements next to the plain forms and return one of the two.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1b134-f541-75cc-9cd3-8f0206f0ec3f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant