Skip to content

perf: parse string fields without per-call decoder and buffer allocations - #1010

Open
jasperpotts wants to merge 9 commits into
993-parser-cleanupsfrom
1008-fast-string-parsing
Open

jasperpotts wants to merge 9 commits into
993-parser-cleanupsfrom
1008-fast-string-parsing

Conversation

@jasperpotts

@jasperpotts jasperpotts commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

What and why

ProtoParserTools.readString used to do four things for every string field it parsed:

  • allocate a heap ByteBuffer and copy the bytes into it;
  • create a new CharsetDecoder;
  • decode into a CharBuffer;
  • copy that into a String.

Performance (AMD EPYC 9275F): parsing string fields is 28-33% faster for ASCII and 12-16% for non-ASCII, and allocates 41-64% less (1,168 → 416 B for a token-create body). Tier A, on by default.

This PR removes those allocations and copies. It is a new tier A item from the review of the first PRs in the series, carried forward from old draft PR #21's idea, and it's always on.

  • Decode straight from the input's backing array when there is one: array-backed BufferedData, or Bytes through parse(Bytes). The array is reached through a new, unexported package, com.hedera.pbj.runtime.internal (ArrayInputs), with a bridge that io.buffer installs when its input classes load. So no public API is widened; the accessors on ByteArrayBufferedData, Bytes and RandomAccessSequenceAdapter stay package-private. Other inputs read the bytes into one array.
  • One shared, strict routine, Utf8Tools.decodeUtf8Strict(byte[], offset, length):
    • returns "" for zero length;
    • if no byte has its top bit set, uses new String(…, ISO_8859_1), a compact Latin-1 string;
    • otherwise runs a validating scan (isWellFormedUtf8), then new String(…, UTF_8). That constructor is only safe after validation, because it replaces malformed input instead of failing.
  • Malformed UTF-8 is still rejected with MalformedProtobufException, exactly as before.
  • The byte-array parser (perf: byte array parser with in-order fast paths, part 1 #999) now uses the same routine.

Tests

  • Utf8StrictDecodeTest compares the validator and decoder with the JDK decoder in REPORT mode over:
    • every sequence of one and two bytes;
    • every three-byte sequence from lead bytes that can start one, plus a sample of the others;
    • four-byte sequences around their limits;
    • 200,000 random byte strings and random text.
  • The existing string tests (ExtendedUtf8MessageWithStringTest, MalformedMessageTest), the fuzz tests and the protoc comparisons pass.
  • pbj-core: spotlessCheck qualityGate build passes.
  • pbj-integration-tests: ./gradlew build and -PpbjPerformance=max pass, 326,369 tests each.

Performance: AMD EPYC 9275F, before = #997, after = this PR

StringParseBench: a HAPI TokenCreateTransactionBody with name, symbol and memo, with ASCII or non-ASCII strings, parsed from Bytes and from a BufferedData. 3 forks, -prof gc.

Benchmark Before ns/op After ns/op Change Before B/op After B/op
StringParseBench.parseBufferedData (text=ascii) 145.8 ± 3.2 97.6 ± 4.1 -33.1% 1,168 416
StringParseBench.parseBufferedData (text=utf8) 201.2 ± 4.9 168.1 ± 7.8 -16.4% 1,352 776
StringParseBench.parseBytes (text=ascii) 148.2 ± 3.8 106.1 ± 3.1 -28.4% 1,216 464
StringParseBench.parseBytes (text=utf8) 201.7 ± 5.4 176.4 ± 6.4 -12.5% 1,400 824
  • ASCII strings: 28-33% faster. Non-ASCII strings: 12-16% faster.
  • Allocation per parse is down 41-64%: 1,168 → 416 B for ASCII from a BufferedData.

Generated code size

Unchanged; this is a runtime change.

Stacked on #997. PR #999 (array parser, part 1) is now stacked on this one.

Fixes #1008

🤖 Generated with Claude Code

jasperpotts and others added 5 commits October 29, 2026 19:43
…ions

ProtoParserTools.readString allocated a ByteBuffer, a CharsetDecoder and a CharBuffer for every string
field. It now decodes straight from the input's backing array when there is one (array-backed BufferedData,
or Bytes), reached through an unexported com.hedera.pbj.runtime.internal bridge that io.buffer installs, so
no public API widens; other inputs read the bytes into one array.

The shared Utf8Tools.decodeUtf8Strict returns "" for zero length, a compact Latin-1 String for ASCII, and
otherwise validates the bytes exactly as the JDK decoder does in REPORT mode before decoding them, still
rejecting malformed UTF-8 with MalformedProtobufException. Utf8StrictDecodeTest compares it with the JDK
decoder over every sequence of up to three bytes, four-byte sequences around their limits and random text.
StringParseBench parses a token create body with ASCII and non-ASCII strings.

Fixes #1008

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Refs #1008

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
@jasperpotts jasperpotts self-assigned this Oct 30, 2026
@jasperpotts
jasperpotts requested review from a team as code owners October 30, 2026 03:25
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

JUnit Test Report

   552 files  +1     552 suites  +1   1m 7s ⏱️ +21s
 2 056 tests +4   2 052 ✅ +4   4 💤 ±0  0 ❌ ±0 
11 588 runs  +4  11 560 ✅ +4  28 💤 ±0  0 ❌ ±0 

Results for commit a086f6c. ± Comparison against base commit a288342.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Integration Test Report

    625 files  ±0      625 suites  ±0   46m 40s ⏱️ - 4m 8s
163 885 tests ±0  163 885 ✅ ±0  0 💤 ±0  0 ❌ ±0 
326 476 runs  ±0  326 476 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit a086f6c. ± Comparison against base commit a288342.

♻️ This comment has been updated with latest results.

Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant