GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — perf: trim per-parse allocations that do no work by knutwannheden · Pull Request #2874 · JSQLParser/JSqlParser

Conversation

@knutwannheden

@knutwannheden knutwannheden commented Oct 29, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

Parsing performance.sql, the JMH benchmark's input, allocates 7.17 MB per parse on master, and about 1 MB of that (15%) does no work. The profile taken for #2870 found the sources: collections created on every call of a production but filled only on a rare path, a regex compiled for every LIKE, and function names joined into a string just to compare them.

This PR is separate from #2870 so each PR's benchmark stays clean.

  • Condition() created a deque for leading NOT and ! prefixes on every call, though few conditions are negated. An empty ArrayDeque allocates its 17-slot array up front. The deque is now created at the first NOT or !, as fix: give every parsed object a node spanning its source #2870 already does for the token deque beside it.
  • PrimaryExpression() created two lists on every call, for chained signs such as - -1 and for JSON operators such as ->. Both are now created at their first element.
  • LikeExpression.KeyWord.from called String.replaceAll("\\s+", "_"), which compiles the pattern on every call. The pattern is now a static constant.
  • consumeXmlLeadingKeyword and FunctionArgumentList called Function.getName() for every function to check for XMLPARSE, XMLELEMENT or XMLFOREST, and getName() joins the name parts into a new string each time. Both now compare the single name part.

Behaviour is unchanged, so there are no new tests. ExpressionPrecedenceTest pins stacked prefixes (not !1 = 1 deparses as NOT ! 1 = 1), DuckDBTest the chained signs, JsonExpressionRenderingTest the JSON operators, XmlForestTest the XML keywords and LikeExpressionTest the multi-word SIMILAR TO.

./gradlew jmh -PjmhProfiler=gc, alternating master (a075737) and this branch on the same machine. gc.alloc.rate.norm is bytes per parse:

run master this branch 5.4 (master run / branch run)
1 time 39.07 ± 2.05 ms/op 38.19 ± 1.39 ms/op 49.52 / 48.21 ms/op
1 alloc 7,166,709 B/op 6,118,424 B/op (−14.6%) 7,767,862 / 7,751,289
2 time 39.56 ± 1.90 ms/op 38.55 ± 1.84 ms/op 48.71 / 49.66 ms/op
2 alloc 7,183,726 B/op 6,084,685 B/op (−15.3%) 7,784,537 / 7,767,974

The branch is 2.4% faster in both runs while the 5.4 control moves 2% either way, so the time difference is at the edge of what this benchmark resolves. The allocation difference is not in doubt.

The Condition() change edits the same lines as #2870, so whichever merges second has a small conflict to resolve. Both deques can then be created together at the first NOT or !.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
knutwannheden added 4 commits October 29, 2026 07:39
@knutwannheden
perf: create the deque of NOT prefixes only for a negated condition
270d861
`Condition()` created a deque for leading `NOT` and `!` prefixes on every
call, though few conditions have one. An empty `ArrayDeque` allocates its
17-slot array up front, so parsing `performance.sql` allocated 585 KB per
parse for deques that stayed empty. The deque is now created at the first
`NOT` or `!`.
@knutwannheden
perf: precompile the whitespace pattern of LikeExpression.KeyWord.from
d192157
`KeyWord.from` runs for every `LIKE`-family condition the parser builds,
and `String.replaceAll` compiled its `\s+` pattern on each call. Parsing
`performance.sql` allocated 201 KB per parse for those compilations. The
pattern is now a precompiled constant.
@knutwannheden
perf: create PrimaryExpression's sign and JSON operator lists on firs…
a6624b7
…t use

`PrimaryExpression()` created two lists on every call: one for chained
signs such as `- -1` and one for JSON operators such as `->`. Nearly
every primary expression has neither, so both lists usually stayed
empty. They are now created at their first element.
@knutwannheden
perf: match XML function names without joining the name parts
d896a6a
`consumeXmlLeadingKeyword` and `FunctionArgumentList` called
`Function.getName()` for every function to check for `XMLPARSE`,
`XMLELEMENT` or `XMLFOREST`, and `getName()` joins the name parts into
a new string on each call. Both checks now compare the single name part.
@knutwannheden knutwannheden changed the title perf: drop an empty deque per condition and a regex compile per LIKE perf: trim per-parse allocations that do no work Oct 29, 2026
@manticore-projects
manticore-projects merged commit 4b983b2 into JSQLParser:master Oct 31, 2026
10 checks passed

Uh oh!

There was an error while loading. Please reload this page.

@manticore-projects

manticore-projects commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

Good work, thank you much for tracing and improving this!

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

knutwannheden added a commit to knutwannheden/JSqlParser that referenced this pull request Oct 31, 2026
@knutwannheden
Merge branch 'master' into fix/binary-operator-ast-span
9f12b5f
Conflicts in JSqlParserCC.jjt, resolved by keeping master's logic and
re-adding the node links on top:

- PARTITION BY (JSQLParser#2878): the four sites call ComplexExpressionList()
  directly and still link the PARTITION BY node.
- PrimaryExpression (JSQLParser#2874): innerSigns is null without an inner sign,
  so the loop that wraps and links signed values is guarded.
- Condition (JSQLParser#2874): one lazily created deque of NOT tokens carries both
  the token for the node and whether it was spelled "!".
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

No reviews

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

2 participants

@knutwannheden @manticore-projects
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.