Skip to content

Add large payload bench - #311

Open
SerafimArts wants to merge 1 commit into
phpstan:2.3.xfrom
SerafimArts:feat/large-payload-bench
Open

Add large payload bench#311
SerafimArts wants to merge 1 commit into
phpstan:2.3.xfrom
SerafimArts:feat/large-payload-bench

Conversation

@SerafimArts

@SerafimArts SerafimArts commented Aug 30, 2026

Copy link
Copy Markdown

Please note that the phplrt implementation doesn't build an AST, as I didn't want to touch original grammar, which adds about 10% to the speed.

The PR only highlights regression and instability in the PHPStan type parser with increasing input payload size, as @ondrejmirtes requested.

Results (~14x times slower):

phplrt4: 961.932ms
phpstan: 13,201.727ms

P.S. In case of size increase (~28x times slower):

// - for ($i = 0; $i < 1000; ++$i) {
// + for ($i = 0; $i < 2000; ++$i) {

phplrt4: 1,941.367ms
phpstan: 54,044.375ms

etc.

@ondrejmirtes

Copy link
Copy Markdown
Member

The problem with PHPDocs is that in a lot of cases you're dealing with small strings, not huge strings. Could you make phplrt faster so that it also wins on small texts?

@JanTvrdik

JanTvrdik commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

You can extract all phpdoc from some popular libraries (symfony, doctrine, ...) and benchmark time to parse all of them to get realistic performance impact.

Generated parsers should be faster in general, but they need to be carefully optimized for PHP performance characteristics, which are very different from C performance characteristics.

@SerafimArts

SerafimArts commented Aug 31, 2026

Copy link
Copy Markdown
Author

The problem with PHPDocs is that in a lot of cases you're dealing with small strings, not huge strings.

Yes, as I said in previous issue:

However, such types are extremely rare, except perhaps Symfony Config or similar.

Could you make phplrt faster so that it also wins on small texts?

In short, it's almost impossible without adding an alternative runtime, only getting close. That's why I started with replacing Hoa in the previous issue. I left PHPDoc parsing for "future features".

In more detail, the lexer:

  1. You have a handwritten one that returns associative arrays of "value", "type", and "line".
  2. In phplrt, it's generated. It returns objects containing the "value", "type", "offset", "channel" (tag), "length", and "name" of the token.

In direct comparison, PCRE2 itself is ~15% faster in phplrt, but due to the objects and amount of data, it takes ~30% more time to construct token objects, assign, analyze, etc.

So:

size phplrt phpstan
20b 1.177ms 0.603ms faster
200b 2.794ms 2.326ms faster
2000b 18.979ms 19.519ms slower
20000b 191.325ms 217.389ms slower

I have no idea why PHPStan is slowing down on 2kb+, since PHPStan's algo is close to perfect.


But the algo is slightly different from here:

  1. PHPStan builds the AST "in place" and trims excess data using "isCurrentTokenType()" checks. The result is a pure PEG/Parser-Combinator.

phplrt works exactly the same, but:

  • Each state additionally has a set of "rule -> token ID list" matches, so instead of checking "isCurrentTokenType()", it checks for a match using "isset" directly within the rule.

At this stage, the algo are almost identical, and we get something like:

phpstan phplrt
13.09ms 11.58ms
  1. Instead of "create node", like "new Ast\Type\Xxx()" in place, phplrt builds a trace:
  • I found a union
  • I found a generic
  • I found a simple type
  • The simple type has been successfully completed
  • Generic is completed
  • etc...

This avoids creating unnecessary AST objects multiple times in cases where the parser doesn't yet know what's going on.

This is precisely why PHPLRT is faster on large datasets, as the current PHPStan implementation has a lot of rollbacks, which lead to unnecessary checks and unnecessary AST constructions (but this isn't certain), like:

  • tryParseCallable
  • tryParseArrayOrOffsetAccess
  • etc.

But this is also a downside, since phplrt then starts building an AST from the trace. But PHPStan already has one ready.

phpstan phplrt
0ms 7.8ms

To summarize, phplrt performs 2 parsing cycles, while PHPStan only performs 1.

This makes phplrt slower on small datasets but faster on large ones (because it simply doesn't do extra work during unnecessary rollbacks). However, it does perform "extra work" upfront to prevent this from happening.

It also saves additional information about tracing, "expected tokens," and other things, which adds another 10-20% to the processing time needed to get meaningful errors, like this:

изображение

It also operates on objects, which adds another layer...

I have some ideas on how to speed things up, for example, by implementing a second runtime (effectively duplicating it) that won't generate a trace but will immediately generate an AST. This will speed things up on small volumes, as it will return one cycle of execution.

Or getting rid of the recursive descent entirely, replacing it with a loop and inlining all the rules - this should speed up the runtime by 10-15% overall, but it will turn the code into complete crap...

In general, I'll try to speed up the parsing even more, but overall the problem is that the functionality that speeds up and makes the parsing more stable is also the same thing that slows down the parsing on small volumes. Something like JIT. It speeds up if there's a lot of data, but slows down if it's a one-time simple operation.

@SerafimArts

Copy link
Copy Markdown
Author

P.S. Well, I also need to lower the PHP version to at least version ^8.1 so that the tool can be considered for future PHPStan 3.x

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.

3 participants