perf(decode): resolve interfaces through cached type flags
The decoder asked every value whether it implements Unmarshaler or encoding.TextUnmarshaler by boxing it into an interface and asserting, which allocated on every scalar field. A per-type flag cache answers first and an interface value is built only where the assertion can succeed; interface destinations are still asked dynamically. A monomorphic hint in front of the cache keeps the hot walk off the sync.Map probe, and it re-points at published cache entries so a miss allocates nothing. Representative document: 233 to 167 allocations; long document typed decode: 107 674 to 63 772 allocations, about 6.1 to about 3.8 ms.
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -22,6 +23,97 @@ func newDecoder() *decoder { return &decoder{} }
|
||||
|
||||
var timeType = reflect.TypeFor[time.Time]()
|
||||
|
||||
var (
|
||||
unmarshalerType = reflect.TypeFor[Unmarshaler]()
|
||||
textUnmarshalerType = reflect.TypeFor[encoding.TextUnmarshaler]()
|
||||
)
|
||||
|
||||
// The per-type flags record which interface lookups a decode into that type
|
||||
// can succeed at, so the hot path consults the cache instead of boxing every
|
||||
// value into an interface to ask. The bits name the receiver the method is
|
||||
// found on: the value itself, or its address.
|
||||
const (
|
||||
flagUnmarshaler uint8 = 1 << iota
|
||||
flagAddrUnmarshaler
|
||||
flagTextUnmarshaler
|
||||
flagAddrTextUnmarshaler
|
||||
)
|
||||
|
||||
// typeFlagCache holds one flag entry per destination type. A set is immutable
|
||||
// once published, the same trade-off structSchemaCache makes; the cache grows
|
||||
// with the number of distinct types decoded, never per document. The hint
|
||||
// below re-points at these published entries, so a hot lookup allocates
|
||||
// nothing.
|
||||
var typeFlagCache sync.Map // reflect.Type -> *flagHintEntry
|
||||
|
||||
// flagHintEntry pairs a type with its cached flags for the monomorphic hint
|
||||
// below. Both caches share the entry shape.
|
||||
type flagHintEntry struct {
|
||||
typ reflect.Type
|
||||
flags uint8
|
||||
}
|
||||
|
||||
// typeFlagHint remembers the entry resolved last, because a decode walks one
|
||||
// type across consecutive fields and elements. A lost race loses only the
|
||||
// hint: every value it can hold came from the cache.
|
||||
var typeFlagHint atomic.Pointer[flagHintEntry]
|
||||
|
||||
func typeFlags(t reflect.Type) uint8 {
|
||||
if e := typeFlagHint.Load(); e != nil && e.typ == t {
|
||||
return e.flags
|
||||
}
|
||||
if v, ok := typeFlagCache.Load(t); ok {
|
||||
entry := v.(*flagHintEntry)
|
||||
typeFlagHint.Store(entry)
|
||||
return entry.flags
|
||||
}
|
||||
var f uint8
|
||||
if t.Implements(unmarshalerType) {
|
||||
f |= flagUnmarshaler
|
||||
}
|
||||
pt := reflect.PointerTo(t)
|
||||
if pt.Implements(unmarshalerType) {
|
||||
f |= flagAddrUnmarshaler
|
||||
}
|
||||
// The date-time types are excluded from the text path: they carry
|
||||
// time.Time's UnmarshalText through an embedded field while their only
|
||||
// accepted form is a bare timestamp.
|
||||
if !isDateTimeType(t) {
|
||||
if t.Implements(textUnmarshalerType) {
|
||||
f |= flagTextUnmarshaler
|
||||
}
|
||||
if pt.Implements(textUnmarshalerType) {
|
||||
f |= flagAddrTextUnmarshaler
|
||||
}
|
||||
}
|
||||
actual, _ := typeFlagCache.LoadOrStore(t, &flagHintEntry{t, f})
|
||||
published := actual.(*flagHintEntry)
|
||||
typeFlagHint.Store(published)
|
||||
return published.flags
|
||||
}
|
||||
|
||||
// unmarshalerOf resolves the Unmarshaler for dst through the flag cache, so
|
||||
// an interface value is built only where the cache says the assertion can
|
||||
// succeed. An interface destination is asked dynamically, because the value
|
||||
// it will hold may implement the interface even when the interface type
|
||||
// itself does not.
|
||||
func unmarshalerOf(dst reflect.Value) (Unmarshaler, bool) {
|
||||
if dst.Kind() == reflect.Interface {
|
||||
u, ok := dst.Interface().(Unmarshaler)
|
||||
return u, ok
|
||||
}
|
||||
f := typeFlags(dst.Type())
|
||||
if f&flagUnmarshaler != 0 {
|
||||
u, ok := dst.Interface().(Unmarshaler)
|
||||
return u, ok
|
||||
}
|
||||
if f&flagAddrUnmarshaler != 0 && dst.CanAddr() {
|
||||
u, ok := dst.Addr().Interface().(Unmarshaler)
|
||||
return u, ok
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (d *decoder) decode(tree map[string]any, v any) error {
|
||||
rv := reflect.ValueOf(v)
|
||||
if rv.Kind() != reflect.Pointer || rv.IsNil() {
|
||||
@@ -109,22 +201,24 @@ func (d *decoder) assign(data any, dst reflect.Value) error {
|
||||
}
|
||||
}
|
||||
|
||||
// textUnmarshalerOf finds the encoding.TextUnmarshaler for dst: on the value
|
||||
// itself, or on its address, so a pointer-receiver UnmarshalText is invoked on
|
||||
// an addressable struct field. The TOML date-time types are excluded, because
|
||||
// they carry time.Time's UnmarshalText through an embedded field while their
|
||||
// only accepted form is a bare timestamp.
|
||||
// textUnmarshalerOf is the same resolution for encoding.TextUnmarshaler,
|
||||
// with the date-time types excluded for the reason typeFlags records.
|
||||
func textUnmarshalerOf(dst reflect.Value) (encoding.TextUnmarshaler, bool) {
|
||||
if !dst.CanInterface() || isDateTimeType(dst.Type()) {
|
||||
return nil, false
|
||||
}
|
||||
if u, ok := dst.Interface().(encoding.TextUnmarshaler); ok {
|
||||
return u, true
|
||||
if dst.Kind() == reflect.Interface {
|
||||
tu, ok := dst.Interface().(encoding.TextUnmarshaler)
|
||||
return tu, ok
|
||||
}
|
||||
if dst.CanAddr() {
|
||||
if u, ok := dst.Addr().Interface().(encoding.TextUnmarshaler); ok {
|
||||
return u, true
|
||||
}
|
||||
f := typeFlags(dst.Type())
|
||||
if f&flagTextUnmarshaler != 0 {
|
||||
tu, ok := dst.Interface().(encoding.TextUnmarshaler)
|
||||
return tu, ok
|
||||
}
|
||||
if f&flagAddrTextUnmarshaler != 0 && dst.CanAddr() {
|
||||
tu, ok := dst.Addr().Interface().(encoding.TextUnmarshaler)
|
||||
return tu, ok
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
@@ -147,6 +241,9 @@ func (d *decoder) assignStruct(tbl map[string]any, dst reflect.Value) error {
|
||||
// deterministically: the smallest one.
|
||||
unknown := ""
|
||||
for key := range tbl {
|
||||
if _, ok := schema.byName[key]; ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := schema.byName[strings.ToLower(key)]; ok {
|
||||
continue
|
||||
}
|
||||
@@ -159,7 +256,12 @@ func (d *decoder) assignStruct(tbl map[string]any, dst reflect.Value) error {
|
||||
}
|
||||
}
|
||||
for key, val := range tbl {
|
||||
field, ok := schema.byName[strings.ToLower(key)]
|
||||
// A key that is already lowercase, which document keys usually are,
|
||||
// hits the map directly; only a miss pays for the case fold.
|
||||
field, ok := schema.byName[key]
|
||||
if !ok {
|
||||
field, ok = schema.byName[strings.ToLower(key)]
|
||||
}
|
||||
if !ok {
|
||||
if schema.embedMaps != nil {
|
||||
// Leftover keys land in an untagged embedded map, the inverse
|
||||
|
||||
Reference in New Issue
Block a user