From 58de55ee902cea40b739204e31720492d7bd1d5e Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 21 Oct 2026 22:46:59 +0200 Subject: [PATCH 1/2] [Performance] Short-circuit NodeComparator::areNodesEqual before printing Bail out early in areNodesEqual() before the expensive printWithoutComments() call: - return false when either node is null (also avoids printing a null node) - return false when the two single nodes are of a different class, since nodes of different classes can never print as equal This skips the print+compare round-trip on the common unequal case. --- src/PhpParser/Comparing/NodeComparator.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/PhpParser/Comparing/NodeComparator.php b/src/PhpParser/Comparing/NodeComparator.php index b944e1da94c..918117e680d 100644 --- a/src/PhpParser/Comparing/NodeComparator.php +++ b/src/PhpParser/Comparing/NodeComparator.php @@ -34,6 +34,10 @@ public function printWithoutComments(Node|array|null $node): string */ public function areNodesEqual(Node|array|null $firstNode, Node|array|null $secondNode): bool { + if ($firstNode === null || $secondNode === null) { + return false; + } + if ($firstNode instanceof Node && ! $secondNode instanceof Node) { return false; } @@ -47,6 +51,10 @@ public function areNodesEqual(Node|array|null $firstNode, Node|array|null $secon } if (! is_array($secondNode)) { + if ($firstNode::class !== $secondNode::class) { + return false; + } + return $this->printWithoutComments($firstNode) === $this->printWithoutComments($secondNode); } From 52d3a370dd50f933e96c9705d49af0d20e7dda53 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 21 Oct 2026 22:53:29 +0200 Subject: [PATCH 2/2] Add bench for NodeComparator::areNodesEqual short-circuit --- bench/node_comparator_short_circuit_bench.php | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 bench/node_comparator_short_circuit_bench.php diff --git a/bench/node_comparator_short_circuit_bench.php b/bench/node_comparator_short_circuit_bench.php new file mode 100644 index 00000000000..06a5bba3ada --- /dev/null +++ b/bench/node_comparator_short_circuit_bench.php @@ -0,0 +1,84 @@ +create(); + +/** @var NodeComparator $nodeComparator */ +$nodeComparator = $rectorConfig->make(NodeComparator::class); + +// realistic blend of pairs; most are unequal, the common hot-path case +$pairs = [ + // null operand - newly short-circuited + [new Variable('value'), null], + [null, new String_('name')], + // different class single nodes - newly short-circuited + [new Variable('value'), new String_('value')], + [new Identifier('foo'), new LNumber(1)], + [new String_('a'), new Variable('a')], + // same class, unequal - unchanged (still printed) + [new Variable('first'), new Variable('second')], + [new String_('alpha'), new String_('beta')], + // same class, equal - unchanged (still printed) + [new Variable('same'), new Variable('same')], + [new Identifier('id'), new Identifier('id')], +]; + +$cpuSeconds = static function (): float { + $usage = getrusage(); + return $usage['ru_utime.tv_sec'] + $usage['ru_utime.tv_usec'] / 1_000_000 + + $usage['ru_stime.tv_sec'] + $usage['ru_stime.tv_usec'] / 1_000_000; +}; + +$run = static function () use ($nodeComparator, $pairs): void { + foreach ($pairs as [$first, $second]) { + $nodeComparator->areNodesEqual($first, $second); + } +}; + +// warm up +$run(); + +$wallStart = microtime(true); +$cpuStart = $cpuSeconds(); + +for ($repeat = 0; $repeat < REPEAT; ++$repeat) { + $run(); +} + +$cpu = $cpuSeconds() - $cpuStart; +$wall = microtime(true) - $wallStart; + +$totalCalls = REPEAT * count($pairs); + +printf('pairs: %d, repeats: %d, calls: %d%s', count($pairs), REPEAT, $totalCalls, PHP_EOL); +printf('CPU: %.4f s (%.4f us/call)%s', $cpu, $cpu / $totalCalls * 1_000_000, PHP_EOL); +printf('wall: %.4f s%s', $wall, PHP_EOL);