perf(decode): resolve interfaces through cached type flags
This commit is contained in:
@@ -88,6 +88,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
halved from 67 664 to 31 765. Date-time tokens are validated by a byte
|
halved from 67 664 to 31 765. Date-time tokens are validated by a byte
|
||||||
scan instead of regular expressions, repeated keys share one string
|
scan instead of regular expressions, repeated keys share one string
|
||||||
across array-of-tables elements, and per-statement buffers are reused.
|
across array-of-tables elements, and per-statement buffers are reused.
|
||||||
|
- Typed decoding is 12 percent faster than in 1.1.0 on the representative
|
||||||
|
document (9792 ns against 11 147 ns) with 24 percent fewer allocations
|
||||||
|
(167 against 220); interface lookups resolve through a cached per-type
|
||||||
|
flag set instead of boxing every value into an interface to ask.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,6 +23,97 @@ func newDecoder() *decoder { return &decoder{} }
|
|||||||
|
|
||||||
var timeType = reflect.TypeFor[time.Time]()
|
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 {
|
func (d *decoder) decode(tree map[string]any, v any) error {
|
||||||
rv := reflect.ValueOf(v)
|
rv := reflect.ValueOf(v)
|
||||||
if rv.Kind() != reflect.Pointer || rv.IsNil() {
|
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
|
// textUnmarshalerOf is the same resolution for encoding.TextUnmarshaler,
|
||||||
// itself, or on its address, so a pointer-receiver UnmarshalText is invoked on
|
// with the date-time types excluded for the reason typeFlags records.
|
||||||
// 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.
|
|
||||||
func textUnmarshalerOf(dst reflect.Value) (encoding.TextUnmarshaler, bool) {
|
func textUnmarshalerOf(dst reflect.Value) (encoding.TextUnmarshaler, bool) {
|
||||||
if !dst.CanInterface() || isDateTimeType(dst.Type()) {
|
if !dst.CanInterface() || isDateTimeType(dst.Type()) {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
if u, ok := dst.Interface().(encoding.TextUnmarshaler); ok {
|
if dst.Kind() == reflect.Interface {
|
||||||
return u, true
|
tu, ok := dst.Interface().(encoding.TextUnmarshaler)
|
||||||
|
return tu, ok
|
||||||
}
|
}
|
||||||
if dst.CanAddr() {
|
f := typeFlags(dst.Type())
|
||||||
if u, ok := dst.Addr().Interface().(encoding.TextUnmarshaler); ok {
|
if f&flagTextUnmarshaler != 0 {
|
||||||
return u, true
|
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
|
return nil, false
|
||||||
}
|
}
|
||||||
@@ -147,6 +241,9 @@ func (d *decoder) assignStruct(tbl map[string]any, dst reflect.Value) error {
|
|||||||
// deterministically: the smallest one.
|
// deterministically: the smallest one.
|
||||||
unknown := ""
|
unknown := ""
|
||||||
for key := range tbl {
|
for key := range tbl {
|
||||||
|
if _, ok := schema.byName[key]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if _, ok := schema.byName[strings.ToLower(key)]; ok {
|
if _, ok := schema.byName[strings.ToLower(key)]; ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -159,7 +256,12 @@ func (d *decoder) assignStruct(tbl map[string]any, dst reflect.Value) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for key, val := range tbl {
|
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 !ok {
|
||||||
if schema.embedMaps != nil {
|
if schema.embedMaps != nil {
|
||||||
// Leftover keys land in an untagged embedded map, the inverse
|
// Leftover keys land in an untagged embedded map, the inverse
|
||||||
|
|||||||
Reference in New Issue
Block a user