Skip to content

Read ASCII strings without creating a subarray in BinaryReader - #1566

Open
intech wants to merge 1 commit into
bufbuild:mainfrom
Connectum-Framework:up/reader-string
Open

intech wants to merge 1 commit into
bufbuild:mainfrom
Connectum-Framework:up/reader-string

Conversation

@intech

@intech intech commented Oct 10, 2026

Copy link
Copy Markdown

Follow-up to the suggestion in #1509 to run the ASCII fast path on this.buf.

BinaryReader.string() read its bytes through bytes(), which creates a subarray view for every string. The ASCII fast path only needs to read those bytes.

With this change:

  • The ASCII fast path reads the bytes from the buffer directly.
  • Only the UTF-8 decoder still receives a subarray. It handles strings longer than 32 bytes and any string with a byte above 0x7f.
  • Length, bounds check and errors are the same as in bytes().

BinaryReader.string() had no direct tests. The new tests cover:

  • both sides of the fast-path length limit;
  • a non-ASCII byte at the first and the last position of the fast path;
  • the reader position after a string;
  • a reader over a view at a non-zero offset;
  • premature EOF;
  • strict and lenient invalid UTF-8.

Benchmarks

The numbers come from packages/protobuf-bench, unchanged corpus, run on a GitHub-hosted runner (AMD EPYC 7763, Node.js 24.5.0).

  • Method: 30 pairs of fresh processes, before and after, in random order within each pair. Each process is pinned to one CPU.
  • Columns: ns/op is the median over passes of each pass's p50. Δ is the median of the per-pair ratios, and "faster in" counts the pairs in which this change was faster.
  • Threshold: cases with a Benjamini-Hochberg-corrected sign test p ≤ 0.05 and |Δ| ≥ 2.2 %:
case before (ns/op) after (ns/op) Δ ops/s faster in
BinaryReader/string-ascii 85.3 58.2 +46.0 % 30/30
fromBinary/user-normal 2,184 1,714 +27.3 % 30/30
fromBinary/map-scalar 15,134 12,729 +19.5 % 30/30
fromBinary/repeated-scalar 4,574 4,143 +10.4 % 30/30
fromBinary/general 206,201 191,088 +8.4 % 30/30
fromBinary/map-message 1,804,764 1,733,623 +4.1 % 27/30

No significant difference:

  • All other cases of the corpus.
  • BinaryReader/string-utf8. Its path is essentially unchanged: these strings are longer than 32 bytes and go straight to the decoder, as before. Results per run:
run Δ faster in BH p
this run −3.2 % 9/30 0.28
GitHub runner, earlier run −1.1 % 12/30 0.82
GitHub runner, earlier run −0.5 % 15/30 1
local machine +0.7 % 19/30 0.78

We measured the same way on production-shaped payloads as well. The fromBinary gains are:

payload gain
deep stress message +29.1 %
Kubernetes pod list +22.8 %
OTLP metrics +17.5 %
OTLP trace export, 100 spans +16.6 %
OTLP logs +13.3 %
small RPC and GraphQL envelopes +6.5 % to +15.0 %

We can share these fixtures if they are useful.

Size

The bundle-size table is updated. For one file:

  • minified: 67,700 → 67,792 B (+92 B);
  • gzip: 15,940 → 15,926 B (−14 B).

wire/binary-encoding.js minified (esbuild): 7,019 → 7,113 B (+94 B), gzip 2,331 → 2,346 B (+15 B).

🤖 Generated with Claude Code

https://claude.ai/code/session_01JCd4TPygQp4GPxMsTMm2Bi

BinaryReader.string() took its bytes through bytes(), which creates a
subarray view for every string, although the ASCII fast path only needs to
read the bytes. It now reads them from the buffer directly; only the UTF-8
decoder, used for longer strings and for any byte above 0x7f, gets a
subarray. Length, bounds check and errors are those of bytes().

Adds tests for BinaryReader.string(), which had none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JCd4TPygQp4GPxMsTMm2Bi
@vercel

vercel Bot commented Oct 10, 2026

Copy link
Copy Markdown

@intech is attempting to deploy a commit to the bufbuild Team on Vercel.

A member of the Team first needs to authorize it.

@intech

intech commented Oct 10, 2026

Copy link
Copy Markdown
Author

This continues the performance work discussed in #333 and implements the follow-up suggested in #1509 (running the ASCII fast path on this.buf).

A note on how this was made: the code changes and the benchmarks were produced by an AI assistant under my direction, and I reviewed every step.
The full history measurement method, A/A noise calibration, CPU profiles, reviews, and the raw benchmark reports is in our fork:

fork PR: Connectum-Framework#28 (benchmark reports are the bot comments on that PR)

benchmark tooling: Connectum-Framework#26

This branch has not been deployed

No deployments
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