FasterXML / FasterXML/jackson-core

Avoid per-character escape-table load in _writeStringSegment ASCII loop

Open
#1,680 10 comments 0 reactions 0 assignees View on GitHub
Dominant language
Java
Stars
2.4k
Forks
928
Avg merge
2d 18h
Merged PRs (30d)
24

Description

`_writeStringSegment()`'s ASCII fast loop reads `_outputEscapes[ch]` for every character:

```java
while (offset < len) {
int ch = text.charAt(offset);
if (ch > 0x7F || escCodes[ch] != 0) {
break;
}
outputBuffer[outputPtr++] = (byte) ch;
++offset;
}
```

That is a second, character-dependent load on the critical path of every character copied.

### Suggested fix

For the standard escape table the same test is expressible with constants
(`ch < 0x20 || ch > 0x7F || ch == '"' || ch == '\\'`), so the loop can branch on a loop-invariant
flag:

```java
final boolean stdEsc = (escCodes == CharTypes.get7BitOutputEscapes());
```

C2 unswitches the loop on it and constant-folds the flag in each clone, so the standard-table clone
has no per-character table load. A custom quote character, `ESCAPE_FORWARD_SLASHES`, or a
`CharacterEscapes` instance all produce a different array, so the flag is false and the original
lookup runs unchanged.

### Measurements

JDK 25 / x86_64, one String property per object, 3 forks, 5x5 iterations, pinned. Removing the load
shortens the loop body enough for C2 to unroll it twice as far:

| | insns | `(%rsp)` operands | unroll | insns/char | stack ops/char |
|---|---|---|---|---|---|
| without | 31 | 4 | x2 | 15.5 | 2.0 |
| with | 55 | 4 | **x4** | **13.8** | **1.0** |

`SingleBench.serialize`: 356.5 -> 347.5 ns/op (3.1.5), 331.9 -> 318.6 ns/op (2.22.0).

Output is byte-identical: verified over all characters 0x00-0x100, quotes, backslashes, control
characters, forward slash, non-ASCII, and strings crossing the segment boundary.

### Important note on the size of the win

Measure this **after** https://github.com/FasterXML/jackson-databind/issues/6182. With that issue
still present, `String.charAt`'s branch profile is polluted, the copy loop is neither unrolled nor
range-check eliminated, and this change appears to be worth about -23%. Almost all of that is the
pollution rather than the removed table load. With the profile clean it is worth a few percent, as
above.

### Reproducer

https://github.com/franz1981/c2-writestring-spill-jmh -- `SingleBench.serialize`. The README
documents the `perfasm` loop measurements and the before/after numbers. `master` is Jackson 2.22.0;
the `jackson3-write-paths` branch is the same benchmark on 3.1.5.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with the implementation of `_writeStringSegment()` and the linked #6182 context, then run `SingleBench.serialize` from the `c2-writestring-spill-jmh` reproducer after the profile is clean. The change is done when output remains byte-identical across the listed character cases and the benchmark confirms the reported improvement without changing custom escape-table behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
performance
Issue type
Refactor
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.