Skip to content

Optimize StringUtil and HttpURI - #15673

Open
lorban wants to merge 6 commits into
jetty-12.1.xfrom
enhancement/12.1/wendigo-stringutil-httpuri
Open

Optimize StringUtil and HttpURI#15673
lorban wants to merge 6 commits into
jetty-12.1.xfrom
enhancement/12.1/wendigo-stringutil-httpuri

Conversation

@lorban

@lorban lorban commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

The StringUtil and HttpURI parts of #15498 made by @wendigo

asciiToLowerCase and asciiToUpperCase scan for the first character
needing conversion before deciding whether any work is needed at all,
and most strings need none, so that scan decides the common case. It
tested one character per iteration, with a table load and a comparison
each.

Pack 4 characters into a long, one per 16 bit lane, and test all 4 at
once with a SWAR range comparison; the string is returned unchanged as
soon as the scan runs out without a match. The top bit of each lane is
cleared before the additions so they cannot carry into the next lane,
and those lanes are then discarded, as such a character is far outside
the ASCII range. The test can report a false positive for a non ASCII
lane, which a per character confirmation rejects; it never reports a
false negative for an ASCII lane, which is what makes the scan correct.

The two loops of the previous implementation also collapse into one:
scanning backwards for the last uppercase character and converting
everything below it is the same as scanning forwards for the first and
converting everything above it.

Verified against the previous implementations over all 65536
characters, over all 4 character combinations of the values around
every lane and range boundary, and over 2M random strings.
Parsing the path switched on every character and, for the ones that are
not delimiters, consulted two boolean tables: one for whether the
character is legal, one for whether it is suspicious. Almost every
character of a real path is neither, so almost every character paid a
switch dispatch and two table lookups to establish that there was
nothing to do.

Collect the characters that need attention, the delimiters that drive
the state machine plus the illegal and suspicious ones, into a 128 bit
set held as two longs, and test it before the switch. The test is a
shift and a mask of a value the JIT can keep in a register, and it
takes the common character out of the switch entirely. The same test
short circuits validateSegment().

The set is derived from the same tables and delimiters in the static
initializer, so it cannot drift from them.

Verified against the previous implementation over 202k URIs through
three entry points, comparing path, canonical path, decoded path,
param, query, fragment, host, scheme and violations: identical
throughout. The corpus covers every character in several positions,
percent encodings, parameters, dot segments and random strings over an
alphabet of the interesting characters.

Parsing a long path drops 31%, an absolute URI 36%, a typical path with
a query 19%.

Signed-off-by: Ludovic Orban <lorban@bitronix.be>
@lorban lorban self-assigned this Aug 26, 2026
The check for an illegal path character read the table before testing
the index against its length:

    if (c > __pathCharacters.length || !__pathCharacters[c])

The table holds 128 entries, so for c == 128 the first test is false
and the second indexes one past the end. Every other character is
handled: below 128 the index is valid, above 128 the first test short
circuits. Only U+0080 lands in the gap.

The effect is an ArrayIndexOutOfBoundsException instead of an
ILLEGAL_PATH_CHARACTERS violation, so the URI never reaches the
compliance handling that decides what to do about it. A path can carry
that character over HTTP/2, where :path is decoded as ISO-8859-1.

The same expression appears in validateSegment(), fixed as well.

Signed-off-by: Ludovic Orban <lorban@bitronix.be>
Every request parses a URI, from the request line over HTTP/1 and from
the :path pseudo header over HTTP/2, and nothing measured it.

Cover the shapes that behave differently: a short path, a typical path
with a query, a long path, a percent encoded path, a path with a
parameter, and an absolute URI.
HttpURI.Mutable.scheme(HttpScheme) went through scheme(String), which
lowercases via URIUtil.normalizeScheme. All four HttpScheme values are
lowercase already, so the scan can only ever return the string it was
given.

This is on the path of every HTTP/2 request, whose :scheme pseudo header
resolves to an HttpScheme before the URI is built. Verified that the
enum and string paths still produce the same scheme for every value.

Decoding a request in steady state drops a further 4%.
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
@lorban

lorban commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

The StringUtil optimizations are not conclusive IMHO. I've added a StringUtilBenchmark class to assess the perf gain and here is what I got on my machine:

baseline (12.1.x vanilla):

Benchmark                                               Mode  Cnt   Score   Error  Units
StringUtilBenchmark.asciiToLowerCaseLongLateConversion  avgt    5  35.440 ± 0.275  ns/op
StringUtilBenchmark.asciiToLowerCaseLongNoChange        avgt    5  25.769 ± 0.119  ns/op
StringUtilBenchmark.asciiToLowerCaseShortNoChange       avgt    5   0.620 ± 0.006  ns/op
StringUtilBenchmark.asciiToLowerCaseTypicalMixed        avgt    5  15.850 ± 2.770  ns/op
StringUtilBenchmark.asciiToLowerCaseTypicalNoChange     avgt    5   6.333 ± 0.046  ns/op
StringUtilBenchmark.asciiToUpperCaseLongLateConversion  avgt    5  35.460 ± 0.538  ns/op
StringUtilBenchmark.asciiToUpperCaseLongNoChange        avgt    5  25.863 ± 0.437  ns/op
StringUtilBenchmark.asciiToUpperCaseShortNoChange       avgt    5   0.619 ± 0.004  ns/op
StringUtilBenchmark.asciiToUpperCaseTypicalMixed        avgt    5  15.195 ± 0.172  ns/op
StringUtilBenchmark.asciiToUpperCaseTypicalNoChange     avgt    5   6.361 ± 0.119  ns/op

with this PR:

Benchmark                                               Mode  Cnt   Score   Error  Units
StringUtilBenchmark.asciiToLowerCaseLongLateConversion  avgt    5  51.213 ± 0.762  ns/op
StringUtilBenchmark.asciiToLowerCaseLongNoChange        avgt    5  28.003 ± 0.328  ns/op
StringUtilBenchmark.asciiToLowerCaseShortNoChange       avgt    5   0.323 ± 0.001  ns/op
StringUtilBenchmark.asciiToLowerCaseTypicalMixed        avgt    5  14.363 ± 0.378  ns/op
StringUtilBenchmark.asciiToLowerCaseTypicalNoChange     avgt    5   6.383 ± 0.114  ns/op
StringUtilBenchmark.asciiToUpperCaseLongLateConversion  avgt    5  50.875 ± 1.375  ns/op
StringUtilBenchmark.asciiToUpperCaseLongNoChange        avgt    5  27.533 ± 0.288  ns/op
StringUtilBenchmark.asciiToUpperCaseShortNoChange       avgt    5   0.343 ± 0.015  ns/op
StringUtilBenchmark.asciiToUpperCaseTypicalMixed        avgt    5  13.881 ± 0.156  ns/op
StringUtilBenchmark.asciiToUpperCaseTypicalNoChange     avgt    5   6.332 ± 0.107  ns/op

There is a significant degradation for long strings with late conversion and a small degradation for long strings that do not need to be changed, with marginal changes in the other cases.

@wendigo what motivated you to modify the StringUtil.asciiToZZZCase() methods as they don't seem to improve perf by much. Did I miss something?

Comment on lines 1238 to 1246
public Mutable scheme(HttpScheme scheme)
{
return scheme(scheme.asString());
// The known schemes are lowercase already, so skip normalizing them;
// this is on the path of every HTTP/2 request, whose :scheme pseudo
// header resolves to one of them.
_scheme = scheme.asString();
_uri = null;
return this;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why change HTTP/1 behaviors?
These changes should not favor HTTP/2 over HTTP/1.

@sbordet sbordet moved this to 👀 In review in Jetty 12.1.14 Aug 31, 2026
@sbordet sbordet added the Sponsored This issue affects a user with a commercial support agreement label Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Performance Sponsored This issue affects a user with a commercial support agreement

Projects

Status: 👀 In review

Development

Successfully merging this pull request may close these issues.

4 participants