Skip to content

Encoder: look at a short string and write it in one pass - #685

Merged
goccy merged 1 commit into
masterfrom
perf/encoder-short-strings
Sep 29, 2026
Merged

goccy merged 1 commit into
masterfrom
perf/encoder-short-strings

Conversation

@goccy

@goccy goccy commented Sep 29, 2026

Copy link
Copy Markdown
Owner

Summary

A string was looked at for a byte to escape and then, at the end of AppendString, copied from its beginning again: read twice, after a jump to the copy and loads of what the look had not kept in registers. In the encode of the small payload of the benchmark report, whose strings are 6 to 36 bytes, the strings took 0.72 s where the ones of sonic took 0.41 s on amd64.

  • Up to maxOnePassLength bytes in one pass: a string of 1 to 3 bytes (by its bytes), 4 to 7 (by the two halves of a word) and 8 to maxOnePassLength (by 2 to 4 overlapping words) is written to its place in the capacity of the buffer as it is looked at, and AppendString returns from there. If the string has a byte to escape, what was written is left out of the buffer and the escape of the options appends it as before. maxOnePassLength is 31 on amd64 and on the architectures without SIMD, and 15 on arm64, whose SIMD scan starts at 16 bytes. A buffer without the capacity goes the way of before, which grows it.
  • No larger frame: the position in the buffer is taken from buf again after the check for the escapes, which is kept across that call anyway, so that the frame of AppendString stays 0x80 bytes on amd64. A larger frame (0x90) moved the code of the unchanged paths and made them 2-5% slower.
  • One call less for a longer string: AppendString calls scanBytesSIMD, which is inlined, instead of hasEscapeSIMD, which was not and is removed.
  • Empty string first: an empty string is appended before the options are looked at.

BenchmarkAppendString measures each length of 0 to 10000 bytes, 8 to 32 in detail, with the options of the std configuration (HTMLEscapeOption | NormalizeUTF8Option) and of the fastest one (none), and TestAppendStringFastPath covers every length up to 72.

Measurements

BenchmarkAppendString on amd64 CI against master, 3 machines (EPYC 7763), the mean over 8 function layouts of the median of 5 rounds for the lengths outside 8 to 31 (4 layouts of 3 rounds for those); each binary was also built with a pad file (in parentheses), to separate the effect of the change from the one of the layout:

bytes std fastest
0 -14.6% to -15.2% (-9.8% to -10.8%) -18.5% to -18.6% (-14.0% to -14.3%)
3 -17.0% to -17.9% (-19.8% to -20.1%) -22.2% to -22.7% (-26.3% to -26.6%)
6 -15.5% to -16.0% (-16.4% to -17.1%) -12.6% to -12.9% (-11.7% to -12.5%)
8 to 31 -20% to -39% -25% to -41%
32 -2.2% to -3.2% (-1.0% to -2.4%) -4.4% to -4.7% (-3.3% to -3.7%)
36 to 300 -0.7% to -4.8% (-3.2% to +1.0%) -0.8% to -5.8% (-4.5% to +1.1%)
1000 -0.8% to -1.5% (-0.6% to -1.1%) -0.7% to -1.0% (-1.0% to +0.1%)
10000 -0.1% to +0.1% (-0.1% to +0.4%) -0.2% to -0.0% (-0.1% to +0.1%)

Local arm64: -13% to -42% for 0 to 16 bytes, -0.2% to -9% for the longer ones.

The encode of the report payloads alone, amd64 CI, 3 machines (EPYC 7763 x2, EPYC 9V74), 3 layouts, against master: every payload of every configuration is faster, large -8.7% to -11.2%, small -4.6% to -9.8%, twitter-any -4.1% to -9.3%, openai -2.8% to -8.0%, code and github -0.4% to -4.5%. Against sonic:

payload master this change
fastest/small +2.6% to +3.4% -4.2% to -6.9%
fastest/openai +1.1% to +8.1% -0.6% to -2.0%
fast/openai -2.0% to +4.8% -0.1% to -6.7%
fastest/anthropic +2.2% to +11.5% +0.1% to +6.5%

Test

  • TestAppendStringFastPath: every length up to 72, with each byte to escape at each position, against the escape of the options.
  • go test ./... with go1.25 / go1.26 / go1.27.1 and -race, on amd64 too; the tests of the root, the encoder and the decoder on linux/s390x.
  • make lint.

🤖 Generated with Claude Code

A string of 1 to 31 bytes was looked at for a byte to escape and then
copied from the beginning again at the end of AppendString: read twice,
after a jump to the copy and loads of what the look had not kept in
registers. In the encode of the small payload of the benchmark report,
whose strings are 6 to 36 bytes, the strings took 0.72 s where the ones of
sonic took 0.41 s on amd64.

A string of up to maxOnePassLength bytes (31 on amd64 and on the
architectures without SIMD, 15 on arm64, whose SIMD scan starts at 16) is
now written to its place in the capacity of the buffer as it is looked at,
by its bytes, the halves of a word or 2 to 4 words, and AppendString
returns from there; what was written is left out of the buffer if the
string has a byte to escape. The position is taken from the buffer again
after the check for the escapes, so that the frame of AppendString is not
larger than before.

A longer string is scanned by scanBytesSIMD called from AppendString,
which is inlined, instead of through hasEscapeSIMD, which was not: one
call less. An empty string is appended before the options are looked at.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.20%. Comparing base (09cea77) to head (9586552).
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #685      +/-   ##
==========================================
+ Coverage   88.17%   88.20%   +0.03%     
==========================================
  Files          84       84              
  Lines       12905    12943      +38     
==========================================
+ Hits        11379    11417      +38     
  Misses       1526     1526              
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@goccy
goccy merged commit 054ed21 into master Sep 29, 2026
32 checks passed
@goccy
goccy deleted the perf/encoder-short-strings branch September 29, 2026 11:20
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.

2 participants