Repository navigation
perf(tokenizer): build tokens in place instead of copying them by value [W2-10] - #926
Conversation
…ue [W2-10] nextToken and the token readers it dispatches to now write the token into its slot of the output slice through an out pointer, instead of returning a 40 B models.Token that was spilled, copied into a 72 B TokenWithSpan and appended. On amd64 each of those copies reloaded a just-spilled struct with 16-byte loads that cannot be store-forwarded (C0: ~39% of amd64 lexer time on those lines). The slow reference readers and the prefixed/Unicode-escape readers keep their by-value signatures and are bridged with a field-by-field storeToken. A failed slot is zeroed so tokBuf[len:cap] stays zero. Token output, spans and errors are unchanged (goldens pass). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Claude finished @ajitpratap0's task in 23s —— View job Review of PR #926: build tokens in place
I reviewed this by reading the diff. I did not run the test suite, so I haven't checked the " SummaryThe change is mechanical and carefully scoped. Readers on the hot path now write into What looks correct
Suggestions (non-blocking)
SecurityNo new concerns. There is no new unsafe code, bounds handling is unchanged, and the pointer into the slice is not retained across a VerdictLooks good to merge once the amd64 gate confirms the improvement. The suggestions above are optional hardening. |
Perf gate: REGRESSION (shadow mode: report only, this check never fails)Base
Rules: time regresses at >= +3% with p < 0.01 (Mann-Whitney U), or a suite geomean >= +1.5% (per-round geomeans, same test); allocs/op must not increase; B/op must not rise more than 2%; instructions/op must not rise more than 1.5%. An A/A control with more than 10% of benchmarks moving over 3% is noisy: re-run once, then inconclusive (neutral). Gated suite (GOGC=off, GOMAXPROCS=1)
A/A control: 0 of 32 benchmarks moved more than 3%. Instructions per operation (cmd/perfdriver)
Secondary metric: default GC, GOMAXPROCS=1 (informational)
Environmentbenchstat (primary metric) |
Resolve the W2-03 conflict in readPunctuation: the zero-copy @PARAM and @@var fast path (scanAtName) now writes through setToken(out, ...), and the readIdentifier fallback keeps the out-parameter form. CHANGELOG keeps both entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
|
Claude finished @ajitpratap0's task in 17s —— View job Review of PR #926: build tokens in placeI reviewed the diff by reading it ( Correctness
Suggestions (non-blocking)
Performance Security Verdict |
|
Claude finished @ajitpratap0's task in 19s —— View job Review of PR #926: build tokens in placeI read Correctness
Suggestions (non-blocking)
Performance Security Verdict: Good to merge. The suggestions are optional, and #1 is the one I'd fix. |
…all-slices Brings in #926 (tokens built in place) and #930 (O(1) ReleaseAST). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
Summary
New task W2-10 from checkpoint C0. On amd64 the lines that move tokens by value were about 39% of lexer time; on M4 they were about 14%. Those lines are the
nextToken/readIdentifierreturns of the 40 Bmodels.Token, the build of the 72 Bmodels.TokenWithSpan, and theappend. In the amd64 assembly the returned token is spilled with 8-byte stores and then reloaded with 16-byteMOVUPSloads. x86 cannot store-forward those loads, so they stall, and that happens 2-3 times per token.This PR builds each token in place instead:
tokenizegrows the output slice first (slices.Grow, which uses the same growth sizes asappend), takes&tokens[n]and passes&tw.TokentonextToken. Start and End are then written straight into the slot.nextToken,readIdentifier,readPunctuation,readNumber/finishNumber/invalidNumeric,readQuotedIdentifier,readBacktickIdentifier,readQuotedStringandreadTripleQuotedStringnow takeout *models.Tokenand return onlyerror. Success returns go throughsetToken(out, models.Token{...}). It inlines, and the compiler builds the literal directly in*out(checked inGOARCH=amd64 -S: no stack temp and noMOVUPSreload).read*Slow) and the rare prefixed-string andU&readers keep their by-value signatures, so the fast-vs-slow differential tests are unchanged. They are bridged withstoreToken, which copies field by field because a whole-struct copy would bring the stalling reload back.tokBuf[len:cap]-is-zero invariant still holds.Mechanical size:
tokenizer.gohas about 130 return sites rewritten (return models.Token{...}, nilbecamereturn setToken(out, models.Token{...}), andreturn models.Token{}, errbecamereturn err). The new code istoken_out.goplus the loop change.Test Plan
readPunctuationresolved: the zero-copy@param/@@varpath writes throughsetToken), all gates were re-run on9535b597.task check: pass (fmt, vet, lint,test:race; W0-02 goldens unchanged ininternal/goldens)task test:cbinding: passgo test -count=1 ./pkg/compatibility/...: passgo run honnef.co/go/tools/cmd/staticcheck@latest ./pkg/sql/tokenizer/: cleanpkg/sql/tokenizer/token_out_test.go:TestStoreToken_CopiesEveryFielduses reflection, so a field added tomodels.Tokencannot be silently dropped by the bridge.TestTokenize_GrowsPastEstimate:{}pairs make prescan under-estimate the token count, which exercises slot growth on both the public and scratch paths, with exact types, values and spans checked.TestTokenize_ErrorLeavesSpareSlotsZero: 11 error cases, one or more per reader family (quoted, backtick, bracket,U&,E'',N'', triple-quoted, invalid number, unexpected character). Each checks scratch/public error parity, thattokBuf[len:cap]is zero, and that the next call on the same tokenizer matches the public output.Local A/B (Apple M4, shared machine so indicative only):
scripts/perf/ab.sh -r 10 -t 200ms -b './pkg/sql/tokenizer=^BenchmarkTokenize$/^scratch$' -b './pkg/gosqlx=^BenchmarkParseE2E$' origin/main,GOGC=off,-cpu 1, ABBA. Results (first push, basef03332f7): 22 of 22 faster, B/op and allocs/op unchanged.After merging origin/main (
afec16a4, which adds W2-03 and W3-02) the same local A/B gave the same direction (Tokenize/scratch −8.8% to −18.4%, ParseE2E geomean −7.0%), but other jobs on the machine spread the samples by ±20% to ±270%, so ParseE2E did not reach p<0.01 locally. The amd64 gate below is the authoritative measurement.amd64 CI perf gate (authoritative), on the post-merge head against current main
afec16a4: PASS, 30 of 32 gated benchmarks faster, no regression. AMD EPYC 9V74, Go 1.26.1,GOGC=off,-cpu 1, taskset-pinned, 10 ABBA rounds. A/A control: 0 of 32 benchmarks moved more than 3%.Tokenize/scratch(join/complex/insert): −44.1% / −45.8% / −42.1% (p<0.001). The C0 projection was −8% to −12% for the lexer, and the task threshold is ≥3%.ParseE2E: −4.3% (simple_select) to −20.0% (complex). join −16.3%, aggregate −13.8%, insert −18.1%. The C0 projection was −3% to −5%.ParseRW/*/medium−12.2% to −18.3%,Normalize−4.6% to −13.7%,TransformParse−5.5% to −18.9%,ParseFreshjoin/complex −7.8%/−11.4%. Suite geomean −16.1%.f03332f7, before W2-03/W3-02) gave the same picture: suite geomean −15.9%, scratch tokenize −42% to −46%. W3-02 did not absorb this gain.ParseE2E/simple_selectParseE2E/select_whereParseE2E/joinParseE2E/aggregateParseE2E/subqueryParseE2E/cteParseE2E/windowParseE2E/complexParseE2E/insertParseE2E/updateParseE2E/deleteParseRW/postgresql/mediumParseRW/mysql/mediumParseRW/mariadb/mediumParseRW/sqlserver/mediumParseRW/oracle/mediumParseRW/sqlite/mediumParseRW/snowflake/mediumParseRW/clickhouse/mediumParseFresh/simple_selectParseFresh/joinParseFresh/complexPoolGetPut/serialTokenize/scratch/joinTokenize/scratch/complexTokenize/scratch/insertNormalize/simple_selectNormalize/joinNormalize/complexTransformParse/simple_selectTransformParse/joinTransformParse/complexBreaking Changes
None. Only unexported functions change signature. Token output, spans, errors and allocations are identical.
🤖 Generated with Claude Code
https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2