74. Smaller First Chunks for Generated Typed Values
-
Previously,
astNewallocated 256 values for the first struct or terminal of each type, andastSliceallocated 1,024 elements for the first small list. These allocations dominate tinyParseASTinputs. In the revision measured here, each type started with 8 values and doubled to 256; list chunks started at 64 and doubled to 1,024, satisfying each requested length. Lists over 256 elements remained exact-size allocations. A later bootstrap policy allocates separately for each owner’s first three objects and first nonempty small list before these slab sizes apply; see record 098. The direct typed runtime and conversion after Node parsing share this allocator; ordinaryParse,Recognizeand the engine backends do not change. -
Compare baseline
70511faagainst this allocator change on Go 1.27.1, Apple M3 Max, darwin/arm64, using identical accepted inputs and five alternating 300 ms pairs with no competing builds or tests. Medians:Workload Time before → after B/op before → after Allocs/op before → after JSON, 29 bytes, CodePoints 5.047 → 1.067 µs 60,027 → 2,212 7 → 7 JSON, 29 bytes, Bytes 5.307 → 1.143 µs 60,013 → 2,213 7 → 7 CEL, all 27 policies (2,891 bytes) per operation 528.034 → 265.460 µs 3,213,388 → 433,589 283 → 311 JSON, about 256 KiB 3.047 → 3.032 ms 2,461,914 → 2,458,128 234 → 268 CEL, about 256 KiB 19.803 → 19.243 ms 6,688,103 → 6,702,389 537 → 632 Tiny JSON takes 0.211/0.215× the baseline time and 0.037× its bytes, with unchanged allocation counts. The CEL policy aggregate takes 0.503× the time and 0.135× the bytes, adding 28 allocations. These three timing ranges do not overlap. Large JSON/CEL take 0.995/0.972× the time with overlapping ranges, adding 34/95 allocations. The CEL large control allocates about 14 KB more; no large-input speedup is established.
-
The Unicode ownership fixture parses three lists of 1/24/257/1,025 terminals and structs plus an empty list, in 14/152/1,550/6,158 input bytes. The four direct-construction time ratios are 0.193/0.480/0.881/0.976×; only the largest ranges overlap. For conversion after Node parsing they are 0.501/0.757/0.980/1.052×; the last two ranges overlap. The largest conversion median rises 231.395 → 243.324 µs (+5.2%), despite reducing bytes 1,160,112 → 1,126,989. Both paths add 0/7/8/8 allocations at these lengths. Record this cost alongside the small-input gains; range overlap is not a statistical equivalence test.
-
Each owner grows from a 24-byte slice to a 32-byte slice-plus-size state on this architecture. The extra warm-up allocations are bounded by the maximum chunk sizes. Go's JSON compiler diagnostics still inline the
astNewshape but not the largerastSliceshape; timings above include that tradeoff. Input length was rejected as a per-type sizing proxy because it overestimates rare types; the existing maximum list chunk preserves large-output allocation amortization. -
Generated direct/conversion tests cover both units, Unicode spans, value-pointer uniqueness, empty and optional fields, value-growth boundaries, partial list chunks and exact-size large lists. Lists retain capacity equal to length. Appending or parsing a later failing/small input leaves retained results unchanged. Returned arrays are never recycled or cleared. Smaller chunks do not eliminate discarded sibling reachability through a returned pointer; that remains a separate live heap problem. These figures measure cumulative allocation traffic, not retained heap.
-
Reproduce JSON with
cd parsers/json && go test -run '^$' -bench 'Benchmark(SmallParseAST|ParseAST)$' -benchtime=300ms -count=1and CEL withcd parsers/cel && go test -run '^$' -bench 'Benchmark(PoliciesParseAST|LargeParseAST)$' -benchtime=300ms -count=1. Use the final benchmark sources on both production versions. For the ownership workloads, generate both paths withPEGO_TYPED_CHUNK_DIR=/tmp/pego-chunks go test ./internal/engine -run '^TestGeneratedTypedChunkOwnership$' -count=1, using a Go overlay of baselineinternal/engine/gen_types.gowhen building the parent. In each generatedfalse/truedirectory, benchmarkBenchmarkChunkParseASTwith the same 300 ms flags. Alternate five pairs of the prebuilt binaries. Focused raw output remains local; a full-suite checkpoint follows this optimization. -
The subsequent full-suite checkpoint measures clean commit
5610082: 131 cases with three samples each, 457.810 seconds. Compared with the previous98ef6dbsnapshot, typed-workload median times range 0.965–1.056× and bytes 0.967–1.010×. Allocation counts increase by 34/12/38/12/12/88/81/7 for JSON, CSV, XML, Operators Rule, left recursion, minilang conversion, recovery conversion and outline, respectively. These separate full runs do not isolate a speedup or regression from this method; the alternating controls above remain the causal comparison. The current backend, recognition, incremental and stream analysis is recorded under Where PEGO stands.
Same-Revision Allocation Controls
Internal GenOptions.DisableTypedObjectSlabs and
GenOptions.DisableTypedListSlabs independently emit new(T) and
make([]T, n) references. Selection occurs during generation; the default
parser source stays identical and has no runtime selector. Both direct typed
construction and Node conversion use the selected allocator. These controls
disable each entire slab mechanism; they do not reproduce the historical
256/1,024 initial sizes used in the comparison above.
TestGeneratedTypedChunkOwnership tests all four combinations on both typed
paths and position units, including empty lists, growth boundaries, pointer
uniqueness and saved results. TestGeneratedTypedOutputRetentionFixture
separately isolates discarded objects and unused list backing cells. See the
retention measurement method
for exporting the four same-generator fixtures and measuring live heap and
parse time separately. Individual allocation is a measurement reference;
it is not the generated default or a public parser option.