fix: Use a byte-class table for the string escape scan in jwriter - #53
Open
kinyoklion wants to merge 1 commit into
Open
fix: Use a byte-class table for the string escape scan in jwriter#53kinyoklion wants to merge 1 commit into
kinyoklion wants to merge 1 commit into
Conversation
Scanning for the next byte that requires escaping now uses a 256-entry table instead of range and equality comparisons per byte, matching the technique the reader uses. The compiler emits the comparison chain as multiple compare-and-branch pairs per byte, while the table is a single always-cached load; measured on string-heavy benchmarks this is 5-7% faster, with identical output.
kinyoklion
commented
Aug 7, 2026
| start := 0 | ||
| for i := 0; i < len(s); i++ { | ||
| aByte := s[i] | ||
| if aByte >= ' ' && aByte != '"' && aByte != '\\' { |
Member
Author
There was a problem hiding this comment.
When working on the read performance we had a similar shape and a LUT was a clear win in comparison. So I tested it out on the writer and it was also a win here.
kinyoklion
marked this pull request as ready for review
August 7, 2026 22:46
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
SDK-2888
writeQuotedStringscans for the next byte that needs escaping with a range check and two equality comparisons per byte. The Go compiler emits that as multiple compare-and-branch pairs per byte, which caps the throughput of the one-byte-per-iteration scan loop; a 256-entry byte-class table is a single always-L1-resident load plus one branch, and is the technique the reader-side tokenizer rewrite uses. This converts the writer's scan to the same idiom. Output is byte-identical.The table is deliberately not shared with jreader's: the two predicates differ (the reader must stop at bytes >= 0x80 to perform UTF-8 validation, while this writer copies multi-byte characters through verbatim), and a shared-table variant using per-package bit flags measured performance-neutral (-0.04%), so each package keeps its own four-line generated table rather than gaining an internal cross-package dependency. Each table's comment notes the contrast.
Benchmarks
go1.24.3, linux/amd64, interleaved A/B (3 rounds x count 2, benchstat n=6):
The single-short-string case is flat (fixed per-call overhead dominates); the win appears wherever string scanning is a meaningful share of the work. For context, the same table-vs-comparisons choice measures much larger on the reader side (+7% to +40% for the comparison chain), where the scan loop is a bigger fraction of total time.
Testing
Full suite passes including with
-race;golangci-lintclean;BenchmarkWriteObjectToNoOpWriterNoAllocsremains 0 allocs/op. The cross-permutation writer suite exercises the new scan against the same expected encodings, including every escape class and multi-byte content.Note
Overview
writeQuotedStringnow decides whether a byte can be copied verbatim using a 256-entryplainStringCharstable instead of a per-byte range check plus comparisons for"and\.The table marks bytes from
0x20through0xFFas plain except quote and backslash, so UTF-8 multibyte sequences pass through unchanged (unlike the reader’s table, which is documented as intentionally separate). Encoded JSON output is unchanged; benchmarks show modest gains when string scanning dominates (e.g. arrays of strings, objects).Reviewed by Cursor Bugbot for commit 8e8f17e. Bugbot is set up for automated code reviews on this repo. Configure here.