Skip to content

StdDateFormat.<clinit> degrades JIT compilation of UTF8JsonGenerator._writeStringSegment #6182

Description

@franz1981

StdDateFormat's static initializer builds a SimpleDateFormat. That reaches
DecimalFormatSymbols, which calls String.charAt on the locale's per-mille sign "‰" --
a UTF-16 String.

String.charAt has a single process-wide MethodData. C2 uses the callee's MDO when it inlines,
with no per-call-site refinement, and prunes the isLatin1 branch only when the profiled UTF-16
count is exactly zero. That one call therefore leaves a cold StringUTF16.charAt call inside every
ASCII charAt loop compiled afterwards, which sets IdealLoopTree::_has_call and skips
iteration_split_impl -- so no unrolling and no range check elimination. One of the loops affected
is UTF8JsonGenerator._writeStringSegment, Jackson's own ASCII copy loop.

MapperBuilder's static initializer references StdDateFormat.instance, so simply building an
ObjectMapper is enough to trigger it.

Instrumenting String.charAt via --patch-module java.base to dump a stack trace on the UTF-16
branch shows exactly one such call in a whole run:

String.charAt("‰")
  java.text.DecimalFormatSymbols.findNonFormatChar(:864)
  java.text.DecimalFormatSymbols.<init> / NumberFormat.getIntegerInstance
  java.text.SimpleDateFormat.<init>(:631)
  tools.jackson.databind.util.StdDateFormat.<clinit>(:107)
  tools.jackson.databind.cfg.MapperBuilder.<clinit>(:56)
  JsonMapper.builder()

Suggested fix

DATE_FORMAT_RFC1123 is read only by parseAsRFC1123(), so moving it into a holder class defers
the SimpleDateFormat construction. With that, the instrumented count goes from 1 to 0 for
applications that never parse RFC1123 dates.

Measurements

JDK 25 / x86_64, one String property per object, 3 forks, 5x5 iterations, pinned:

before after
jackson 3.1.5 444.2 +/- 5.6 ns/op 356.5 +/- 6.4 (-19.7%)
jackson 2.22.0 426.7 +/- 7.4 ns/op 331.9 +/- 5.2 (-22.2%)

The copy loop goes from 33 instructions with 13 stack operands per character, not unrolled, to
15.5 instructions with 2 stack operands, unrolled by 2.

A Quarkus REST endpoint serializing a list of beans gains 3.8% end to end (125 967 -> 130 768
req/s, 3 interleaved 30 s reps, ranges not overlapping).

Also worth noting: excluding writeString from inlining used to be worth ~22% on this benchmark.
After this change it is worth nothing -- the two variants are equal within error.

Caveat

This defers rather than removes the problem: an application that really does parse RFC1123 dates
still pollutes the profile, just later. The unconditional fix belongs in
java.text.DecimalFormatSymbols.findNonFormatChar, which should not use String.charAt; and
underneath that, one interpreter-era call arguably should not permanently de-optimize every
charAt loop in the process.

Reproducer

https://github.com/franz1981/c2-writestring-spill-jmh -- SingleBench.serialize vs
SingleBench.serializeWriteStringNotInlined. The README documents the mechanism, 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. The StdDateFormat static
initializer and the affected copy loop are identical in both lines.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions