Skip to content

string performance optimisation - #102

Open
sergeevik wants to merge 1 commit into
dashjoin:mainfrom
sergeevik:performance-string
Open

string performance optimisation#102
sergeevik wants to merge 1 commit into
dashjoin:mainfrom
sergeevik:performance-string

Conversation

@sergeevik

@sergeevik sergeevik commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

string and regex optimisation

For string:

  • create string by ""+char created new every time. If use String.valueOf some string get from java cache (memmory optimisation)
  • replace string concationation in cycle on stringbuilder

For regex:

  • pattern compile one time and then use multiple times instead compile every time

JsonParser.java
replace readEscape logic with hex data (remove create string and parse it)

@sergeevik
sergeevik force-pushed the performance-string branch 3 times, most recently from 267a13d to f5a2474 比较 March 24, 2026 23:18
@aeberhart

Copy link
Copy Markdown
Contributor

Thanks for the PR @sergeevik. Have you run some benchmarks on this? I suspect that there won't be much impact since the JVM does a lot of these optimizations already.

@sergeevik

Copy link
Copy Markdown
Contributor Author

@aeberhart Benchmarks are a complex thing. I tried them on simple examples and stumbled upon many JVM optimizations (due to the simplicity of the scenarios).

This is more of a general practice (my experience).

Regarding strings and creating new strings by appending to chars, the JVM might optimize them, but there's no point in waiting for JIT to notice.

String appending in a loop is never optimized by JIT.

Regex compilation is useful because JIT doesn't do it itself. The JVM might cache some things, but not much.

@sergeevik

Copy link
Copy Markdown
Contributor Author

For example look at the Signature.validate method

jetbrains performance alanytics show memmory allocation
biggest on 'match.split' 5GB
image

deep into String split
image
4.8 GB by 'pattern.split'

deep into Pattern.split
image
3.94 on matcher invoke

deep into mather call
image
2.56 on compile invoke

But we split by empty string. wich mean just get char array. If replace this logic on Pattern.compile we save 50% RAM. If replace this by toCharArray save 100% RAM

less gc call

just example. I try collect more preformance check on my data.

@sergeevik

Copy link
Copy Markdown
Contributor Author

In my case. i try two version
first without my changes. max used RAM 5.5 GB
image

second with my changes. max used RAM 4.1
image

seems it's no diff on result used RAM when all data calculated but defferent use
and this less gc activity

But I'm starting to doubt the usefulness of using String.valueOf for char ;

    public static String valueOf(char c) {
        if (COMPACT_STRINGS && StringLatin1.canEncode(c)) {
            return new String(StringLatin1.toBytes(c), LATIN1);
        }
        return new String(StringUTF16.toBytes(c), UTF16);
    }

it seems like it doesn't create fewer objects. So, I'll make a couple of changes and run some more measurements.

@sergeevik
sergeevik force-pushed the performance-string branch from f5a2474 to f54cb43 比较 April 30, 2026 14:28
@sergeevik

Copy link
Copy Markdown
Contributor Author

Hi @aeberhart, I removed my the String.valueOf s—they're probably pointless.

I left the StringBuilder in the loop because it's still not optimized by the JIT.

Also left the regex as constants.

Removed the string handling in the signature calculation and reworked it to use characters (since it's essentially handling characters, but it creates extra strings).

perok added a commit to perok/functional-home-assistant that referenced this pull request Aug 29, 2026
Third of the allocation work in #237, and the one that answers where JSONata's
share of a page render actually comes from.

It is not parsing — transforms are compiled once at startup and reused. It is
EVALUATION: `Signature.validate` matches a function's argument signature with a
regex on every invocation, allocating a `Matcher` each time, and JSONata's own
coercion machinery invokes functions even for an expression with no visible
call. JFR put `Pattern.matcher <- Signature.validate <- JFunction.validate <-
Jsonata.validateArguments` at 36% of ALL allocation in a page render, 99.6% of
the Matcher total. dashjoin/jsonata-java#102 fixes exactly this upstream, but
it is open, not merged, and there is no release carrying it.

Two transform shapes read a value and apply nothing to it — a bare `$state`
and a single `$attr.<name>` — and those can skip JSONata entirely. They are
recognised once, where the expressions are already compiled once:

  jsonataTrivial   766401 -> 30400 B/op   (25x, ~1.28 kB -> ~50 B per eval)
                      191 ->    12 us/op  (16x)

Stringification is the SAME `Transform.asString` the JSONata path uses, not a
reimplementation, so the two cannot drift on how a number, a boolean or an
absent value renders. That is the whole safety argument, so it is a test rather
than a claim: `TransformsSuite` compares the two over strings, empties, ints,
negatives, doubles, whole doubles, a 2^53 long, booleans, JSON null, lists,
objects, quotes, unicode and a missing key — with JSONata itself as the oracle.
A second test pins what is NOT direct (`$attr.a.b`, `$attr."q"`, `$states`, any
operator or call), because the failure mode of this growing into a second
implementation of the language is worse than the allocation it saves.

The benchmark fixtures deliberately use non-trivial transforms, so `page` and
`pageSignals` are unchanged here. The win is per trivial SLOT, and how much a
real dashboard gets depends on how many of its slots are plain `$state` —
which, for `entityCard`'s value, is the common case.


Claude-Session: https://claude.ai/code/session_01SD3BpUXTpWKFzNjSjrCEEa

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
注册 for free to join this conversation on GitHub. Already have an account? 登录 to comment

标签

None yet

项目

None yet

Development

Successfully merging this pull request may close these issues.

2 participants