Fix #1680: overridable escape-scan fast path for JSON generators - #1682
Fix #1680: overridable escape-scan fast path for JSON generators#1682pjfanning wants to merge 1 commit into
Conversation
…rite fast path Extract the ASCII fast-scan loops of UTF8JsonGenerator and WriterBasedJsonGenerator into protected methods (_writeUnescapedAscii / _findFirstToEscape); default implementations specialize the standard escape table into a direct comparison check (loop-invariant, C2-unswitched), and sub-classes with statically known escaping may override with their own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@franz1981 Claude AI suggested this as it keeps the configurability of the escapes but if you keep the standard escapes, it uses the if statement that you suggested. I'll see if I can get jmh numbers. |
|
I got Claude AI to do a quick benchmark for the utf8 generator and it says that for Java 17, there is little gain. There is a measurable gain in Java 21 (8-15% when the hardcoded |
|
Ran #1682 against #1681 on the reproducer at https://github.com/franz1981/c2-writestring-spill-jmh
With a single implementation loaded the virtual call is devirtualized and inlined, so #1682 compiles One thing I did not measure: the bimorphic case. Once a subclass override is loaded the call site is |
|
@franz1981 I checked and the settable character escapes will need to be retained. On JsonFactoryBuilder:
And despite factories being immutable in 3.x, setCharacterEscapes() is still a mutable setter on the generator itself — it can be called mid-stream (the JsonGeneratorBase._characterEscapes field comment says it can't be final because of JSONP-style use cases). So the table can even change during a generator's lifetime. |
|
@pjfanning that spread may be profile state rather than JDK version. The size of the win depends on whether On the reproducer (jackson-core 3.1.5, JDK 25,
A benchmark that only ever writes ASCII keeps the profile clean and so measures the small number; a FasterXML/jackson-databind#6182 is one common way it gets polluted: |
|
@pjfanning if you are converging to an hybrid solution which can still do the best of the 2 worlds I am happy to close my PR ❤️ Some numbers on how the two changes interact, in case it's useful for the discussion. Setup: a real Quarkus app doing reflection-free Jackson serialization (
Total +7.9%. The C2 assembly explains why. Numbers below are for the ASCII copy loop as it appears in the method that actually runs -- the generated serializer, which inlines
Without #6182 the loop is not unrolled and is badly register-starved: 15 stack accesses to emit one character, with the Removing the escape-table lookup is a separate win on top: it takes the per-character So the escape-table change doesn't look redundant once the profile pollution is fixed -- the two address different costs, and the +2.7% is measured with #6182 already applied. Standalone JMH reproducer with the same three configurations, on both Jackson 2.22.0 and 3.1.5: https://github.com/franz1981/c2-writestring-spill-jmh The compiled copy loop there is identical in the two Jackson versions: 1 char/iteration and 13 stack accesses per character with neither fix, 2 chars/iteration and 2 with the databind fix, 4 chars/iteration and no table load with both. On the inlined benchmark #6183 is worth ~20% and #1681 a further 3-5%, and the not-inlined control never regresses. |
Alternative/extension to #1681 (by @franz1981), targeting
3.xsince it adds newprotectedAPI.Extracts the ASCII fast-scan loops into overridable
protectedmethods, one virtual call per segment (per-character hooks would defeat the optimization once any override is loaded):UTF8JsonGenerator._writeUnescapedAscii(char[]/String, ...)— scan+copy, used by both_writeStringSegmentfast loopsWriterBasedJsonGenerator._findFirstToEscape(char[], int, int)— scan only, used by_writeString2,_writeSegment,_writeString(char[],...)Default implementations include #1681's loop-invariant specialization: when
_outputEscapes == CharTypes.get7BitOutputEscapes()the loop testsch < 0x20 || ch > 0x7F || ch == '"' || ch == '\\'directly (no table load; C2 unswitches on the invariant). CustomCharacterEscapes,ESCAPE_FORWARD_SLASHESand alternate quote chars keep the original table lookup, so output is unchanged everywhere. Sub-classes with statically known escaping rules can override with a simple||check of their own.Relative to #1681: also covers
WriterBasedJsonGenerator, and exposes the extension point; happy to rebase on top of #1681 if that lands in3.1first.Fixes #1680
🤖 Generated with Claude Code