FasterXML / FasterXML/jackson-core
Avoid per-character escape-table load in _writeStringSegment ASCII loop
- Langage dominant
- Java
- Étoiles
- 2.4k
- Forks
- 928
- Merge moyen
- 2 j 18 h
- PR mergées (30 j)
- 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.
Guide de contribution
Aucun guide de contribution indexé pour ce dépôt
Évaluation
Cette issue n'a pas encore été évaluée.