perf(encode): emit in place, pool the buffer and cache interface flags
Test / test (push) Successful in 1m33s
Test / test (push) Successful in 1m33s
Grouped emission walks the entries slice in three passes instead of copying them into per-kind slices, error paths render their key path only when an error names it, inline tables are measured once because a measuring encoder no longer nests another measurement, clean string runs are written in one write, and date-times render through the single-buffer path. The output buffer comes from a sync.Pool and returns to it within a 1 MiB retention cap, the output handed to the caller as a copy; repeated marshals keep the live heap flat, verified over 500 runs. The encoder resolves Marshaler and the text interfaces through the same per-type flag cache and hint the decoder uses, and docs/ARCHITECTURE.md now lists the caches and the pool as the library's shared state. Representative document: 177 to 141 allocations, 12 230 to 6170 bytes per operation; long document: 97 175 to 63 660 allocations, 6.97 to 2.79 MB per operation, about 7.8 to about 3.4 ms.
This commit is contained in:
+16
-8
@@ -102,15 +102,23 @@ sequenceDiagram
|
||||
setter methods are not, and must finish before the value is shared.
|
||||
- The parser is allocated per `ParseContext` call; the parser itself caches
|
||||
nothing between documents.
|
||||
- The one piece of shared state is the struct-schema cache in `decode.go`: a
|
||||
`sync.Map` keyed by `reflect.Type`, holding the flattened field layout the
|
||||
decoder and the encoder both consult. A schema is immutable once published,
|
||||
so concurrent callers only race to build an identical value, the same
|
||||
trade-off `encoding/json`'s field cache makes. The cache grows with the
|
||||
number of distinct struct types, never with document size.
|
||||
- The shared state is a set of caches and pools whose entries are immutable
|
||||
once published, each growing with the number of distinct types rather than
|
||||
with document size: the struct-schema cache in `decode.go` (a `sync.Map`
|
||||
keyed on `reflect.Type`, holding the flattened field layout the decoder and
|
||||
the encoder both consult), the per-type interface flag caches in `decode.go`
|
||||
and `encode.go` (recording where `Marshaler`, `Unmarshaler` and the text
|
||||
interfaces can be found, so a walk builds an interface value only where the
|
||||
assertion can succeed), each fronted by a monomorphic hint holding the type
|
||||
resolved last, and the encoder's output-buffer pool in `encode.go`
|
||||
(`sync.Pool`, buffers returned to it only within a 1 MiB retention cap). A
|
||||
published schema or flag set never mutates, so concurrent callers only race
|
||||
to build an identical value, the same trade-off `encoding/json`'s field
|
||||
cache makes.
|
||||
- The date-time wrappers are values, not pointers, and are immutable in use.
|
||||
- Nothing in the library starts goroutines; apart from the schema cache above,
|
||||
which never mutates a published entry, there is no shared mutable state.
|
||||
- Nothing in the library starts goroutines; apart from the caches and the pool
|
||||
above, which never mutate a published entry, there is no shared mutable
|
||||
state.
|
||||
|
||||
## Dependencies
|
||||
|
||||
|
||||
Reference in New Issue
Block a user