perf(parser): reduce allocations in the parse hot path - #477
Open
Pastalikek65 wants to merge 1 commit into
Open
Conversation
Three small changes cut parser allocations ~8% and bytes ~9% on the microc benchmark (59694 -> 55054 allocs/op, 4572762 -> 4161671 B/op) without changing observable behaviour. 1. Store deferred field captures by value instead of per-element heap allocations. apply is []contextFieldSet (values) rather than []*contextFieldSet, so each capture no longer allocates a separate contextFieldSet object on the heap; the slice backing array is the only allocation and it amortises across the parse. 2. Shrink contextFieldSet by storing field as *structLexerField. The field comes from the immutable grammar node owned by the parser, so a pointer is safe and the struct drops from ~200B to ~88B, halving the cost of slice growth copies. 3. Decouple the optional parse trace from parseContext. traceState holds the writer and depth and is only allocated when tracing is enabled; the deferred exit closure now captures only the traceState, never the parse context. This also lets the unused depth field be removed.
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.
Reduces allocations in the parser hot path.
Results (microc benchmark)
ns/op was noisy on the test machine; the allocation/byte reductions are stable and directly reduce GC pressure, which is the dominant cost on this benchmark's CPU profile (mallocgc ≈ 27% of samples).
Changes
apply []contextFieldSetby value — previously[]*contextFieldSet, which heap-allocated acontextFieldSetper capture. Now captures are stored inline in the slice; the only allocation is the amortised slice growth.contextFieldSet.fieldis now*structLexerField— the field comes from the immutable grammar node owned by the parser for its whole lifetime, so a pointer is safe. ShrinkscontextFieldSetfrom ~200B to ~88B, halving the cost of slice-growth copies (Deferwas 5.5k allocs/op).Decouple the optional trace from
parseContext— newtraceStateholds the writer and depth, allocated only whenTraceis enabled. The deferred exit closure now captures onlytraceState, never the parse context, and the now-unuseddepthfield is removed.Notes on what I looked at and didn't do
The largest remaining allocation is the ~10.8k/op of
parseContextbranch copies indisjunction/group. These escape to the heap becauseparseableandcustomnodes hand&ctx.PeekingLexerto user code /reflect.Call, which forces Go's escape analysis to treat everynode.Parse(ctx, ...)interface call as potentially retainingctx. Eliminating those allocations would require pooling or API changes that risk incorrect behaviour if user code retains the lexer, so they're out of scope here.Validation
go test ./...passes (full suite, including the trace tests viaparticiple.Trace).go vetclean for the changed package (pre-existing struct-tag warnings in tests remain).gofmtclean.