Skip to content

[Java] Strings are written one character at a time from the first character that needs escaping to the end (java, fory-json) #4121

Closed

Description

@pavel-ptashyts

What happens

This is the writing side of #4055. In fory-json, once a string being written contains its first character that
needs escaping, the rest of that string is written one character at a time. The scan that found the character is
never resumed, so the cost of writing a string is proportional to the distance from its first such character to
its end, not to the number of characters that need escaping.

On objects dominated by one large string with a few quotes or newlines in it, this makes writing an order of
magnitude slower than writing the same string without them.

Where

main at f85877f2, both writers:

  • org/apache/fory/json/writer/Utf8JsonWriter.java
    • A compact latin1 String longer than the fixed-size fast paths that contains any byte to escape is rejected
      by writeLongLatin1StringNoEnsure and goes to writeStringChars, which scans it with charAt, four
      characters at a time, up to the first stop.
    • From there writeStringSlow writes every remaining character through writeEscapedChar: a switch, a
      capacity check and one store per character, and it never returns to a bulk copy. The CharSequence
      overload and the UTF16 String path end in the same loop.
  • org/apache/fory/json/writer/StringJsonWriter.java - the same shape in all four tails:
    writeLatin1StringSlow and writeLatin1StringUtf16Slow for compact latin1 input, writeStringSlow and
    writeStringUtf16Slow for character input.

Measurements

Standalone harness, JDK 25.0.2, ForyJson.builder().build(), best of 7 rounds of 5000 writes after 30000 warm-up
writes per variant. The object is a bid with a 19998-character adm string holding VAST XML, pure ASCII, 612
quotes and 345 newlines, the first quote at character 15. The cause is isolated by changing only the
characters that need escaping inside adm, keeping its length:

variant toJsonBytes toJson
nothing to escape 3.9 us 3.6 us
one quote at the very end 19.6 us 3.6 us
one quote at character 1 49.7 us 38.0 us
the real string 46.9 us 43.6 us

With nothing to escape the 20 KB string is written in 3.9 us. One quote near the front costs 13 times that, the
same as all 957 of them, because everything after it is written character by character. For toJsonBytes even a
quote at the very end costs 5 times, because the whole string is first rescanned character by character up to it.

For reference, Jackson 3.1 writeValueAsString on the same object takes 12.5 us without escapes and 25.0 us on
the real string.

Suggested fix

The same shape as #4058, local to the slow paths and leaving the fixed-size fast paths in writeString untouched:

  1. In Utf8JsonWriter, send a compact latin1 string that the fast paths reject to a loop over its bytes that
    copies each plain run a word at a time with the existing isJsonAsciiWord predicate, writes the one byte that
    stopped it through writeEscapedChar, and resumes.
  2. In every character tail of both writers, copy the run of plain characters up to the next stop with one
    capacity check per run, then handle the stop character exactly as now. In StringJsonWriter the run follows
    the current output coder, since a character above 0xFF upgrades the output to UTF16 in the middle of a string.

Measured with that applied to main, same harness, output byte-identical in every variant:

variant toJsonBytes toJson
nothing to escape 3.9 us 3.5 us
one quote at the very end 6.0 us 3.4 us
one quote at character 1 4.5 us 3.8 us
the real string 10.3 us 11.8 us

I have the patch and will open a pull request against main.

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

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions