mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 15:28:54 +00:00
tpl/collections: Speed up where and sort performance
```bash
goos: darwin
goarch: arm64
pkg: github.com/gohugoio/hugo/tpl/collections
cpu: Apple M1 Pro
│ master.bench │ fix-whereperfalloc.bench │
│ sec/op │ sec/op vs base │
WhereOps/eq-10 139.62µ ± 3% 66.04µ ± 2% -52.70% (p=0.002 n=6)
WhereOps/ne-10 139.54µ ± 1% 65.68µ ± 2% -52.93% (p=0.002 n=6)
WhereOps/like-10 179.8µ ± 3% 106.7µ ± 1% -40.66% (p=0.002 n=6)
WhereSliceOfStructPointersWithMethod-10 350.7µ ± 1% 252.2µ ± 2% -28.09% (p=0.002 n=6)
WhereAndSortPages/Where-10 120.93µ ± 1% 81.69µ ± 1% -32.45% (p=0.002 n=6)
WhereAndSortPages/Sort-10 1.608m ± 1% 1.165m ± 1% -27.52% (p=0.002 n=6)
geomean 117.2µ 149.4µ -40.03% ¹
│ master.bench │ fix-whereperfalloc.bench │
│ B/op │ B/op vs base │
WhereOps/eq-10 52.18Ki ± 0% 36.62Ki ± 0% -29.82% (p=0.002 n=6)
WhereOps/ne-10 52.18Ki ± 0% 36.62Ki ± 0% -29.82% (p=0.002 n=6)
WhereOps/like-10 52.20Ki ± 0% 36.63Ki ± 0% -29.83% (p=0.002 n=6)
WhereSliceOfStructPointersWithMethod-10 64.52Ki ± 0% 64.59Ki ± 0% +0.12% (p=0.002 n=6)
WhereAndSortPages/Where-10 21.05Ki ± 0% 21.11Ki ± 0% +0.30% (p=0.002 n=6)
WhereAndSortPages/Sort-10 591.15Ki ± 0% 51.82Ki ± 0% -91.23% (p=0.002 n=6)
geomean 7.690Ki 38.91Ki -44.13% ¹
│ master.bench │ fix-whereperfalloc.bench │
│ allocs/op │ allocs/op vs base │
WhereOps/eq-10 2.515k ± 0% 1.517k ± 0% -39.68% (p=0.002 n=6)
WhereOps/ne-10 2.515k ± 0% 1.517k ± 0% -39.68% (p=0.002 n=6)
WhereOps/like-10 2.515k ± 0% 1.517k ± 0% -39.68% (p=0.002 n=6)
WhereSliceOfStructPointersWithMethod-10 4.503k ± 0% 4.504k ± 0% +0.02% (p=0.002 n=6)
WhereAndSortPages/Where-10 1.029k ± 0% 1.030k ± 0% +0.10% (p=0.002 n=6)
WhereAndSortPages/Sort-10 20.736k ± 0% 1.012k ± 0% -95.12% (p=0.002 n=6)
geomean 358.5 1.594k -53.04% ¹
```
This commit is contained in:
committed by
GitHub
parent
4a641d1594
commit
47e39af2cf
@@ -588,6 +588,8 @@ All.
|
||||
seq := s.RegularPages()
|
||||
ns := s.TemplateStore.GetTemplateFuncsNamespace("collections").(*collections.Namespace)
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
b.Run("Where", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
v, err := ns.Where(context.Background(), seq, "Title", "ge", "Page0480")
|
||||
|
||||
@@ -89,7 +89,7 @@ func (ns *Namespace) Sort(ctx context.Context, l any, args ...any) (any, error)
|
||||
if !v.IsValid() {
|
||||
continue
|
||||
}
|
||||
// Special handling of lower cased hmaps.
|
||||
// Special handling of lower cased maps.
|
||||
if params, ok := v.Interface().(hmaps.Params); ok {
|
||||
v = reflect.ValueOf(params.GetNested(path[i+1:]...))
|
||||
break
|
||||
|
||||
+204
-2
@@ -393,16 +393,219 @@ func parseWhereArgs(args ...any) (mv reflect.Value, op string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// elemResolver resolves a sub-element from a reflect.Value.
|
||||
// Built once before the loop to avoid repeated type checks and reflect.ValueOf allocations.
|
||||
type elemResolver func(reflect.Value) (reflect.Value, error)
|
||||
|
||||
// newElemResolver returns a resolver optimized for the given element type and path.
|
||||
// Returns nil if optimization isn't possible, in which case the caller should
|
||||
// fall back to evaluateSubElem.
|
||||
func (ns *Namespace) newElemResolver(ctxv reflect.Value, elemType reflect.Type, path []string) elemResolver {
|
||||
if elemType.Kind() == reflect.Interface {
|
||||
if len(path) != 1 {
|
||||
return nil
|
||||
}
|
||||
return ns.newInterfaceMethodResolver(ctxv, elemType, path[0])
|
||||
}
|
||||
|
||||
baseType := elemType
|
||||
isPtr := baseType.Kind() == reflect.Pointer
|
||||
if isPtr {
|
||||
baseType = baseType.Elem()
|
||||
}
|
||||
|
||||
if baseType == reflect.TypeFor[hmaps.Params]() {
|
||||
return func(v reflect.Value) (reflect.Value, error) {
|
||||
if isPtr {
|
||||
if v.IsNil() {
|
||||
return zero, nil
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
params := v.Interface().(hmaps.Params)
|
||||
return reflect.ValueOf(params.GetNested(path...)), nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(path) != 1 {
|
||||
return nil
|
||||
}
|
||||
name := path[0]
|
||||
|
||||
// Check for method first, matching evaluateSubElem order.
|
||||
ptrType := baseType
|
||||
if !hreflect.IsInterfaceOrPointer(ptrType.Kind()) {
|
||||
ptrType = reflect.PointerTo(baseType)
|
||||
}
|
||||
mt := hreflect.GetMethodByNameForType(ptrType, name)
|
||||
if mt.Func.IsValid() {
|
||||
return ns.newMethodResolver(ctxv, elemType, mt)
|
||||
}
|
||||
|
||||
switch baseType.Kind() {
|
||||
case reflect.Map:
|
||||
if baseType.Key().Kind() == reflect.String {
|
||||
kv := reflect.ValueOf(name)
|
||||
return func(v reflect.Value) (reflect.Value, error) {
|
||||
if isPtr {
|
||||
if v.IsNil() {
|
||||
return zero, nil
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
return v.MapIndex(kv), nil
|
||||
}
|
||||
}
|
||||
case reflect.Struct:
|
||||
ft, ok := baseType.FieldByName(name)
|
||||
if ok {
|
||||
if ft.PkgPath != "" && !ft.Anonymous {
|
||||
return func(v reflect.Value) (reflect.Value, error) {
|
||||
return zero, fmt.Errorf("%s is an unexported field of struct type %s", name, elemType)
|
||||
}
|
||||
}
|
||||
idx := ft.Index
|
||||
return func(v reflect.Value) (reflect.Value, error) {
|
||||
if isPtr {
|
||||
if v.IsNil() {
|
||||
return zero, nil
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
return v.FieldByIndex(idx), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ns *Namespace) newMethodResolver(ctxv reflect.Value, elemType reflect.Type, mt reflect.Method) elemResolver {
|
||||
if mt.PkgPath != "" {
|
||||
return func(v reflect.Value) (reflect.Value, error) {
|
||||
return zero, fmt.Errorf("%s is an unexported method of type %s", mt.Name, elemType)
|
||||
}
|
||||
}
|
||||
|
||||
numIn := mt.Type.NumIn()
|
||||
maxNumIn := 1
|
||||
needsCtx := numIn > 1 && hreflect.IsContextType(mt.Type.In(1))
|
||||
if needsCtx {
|
||||
maxNumIn = 2
|
||||
}
|
||||
|
||||
switch {
|
||||
case mt.Type.NumIn() > maxNumIn:
|
||||
return nil
|
||||
case mt.Type.NumOut() == 0:
|
||||
return nil
|
||||
case mt.Type.NumOut() > 2:
|
||||
return nil
|
||||
case mt.Type.NumOut() == 1 && mt.Type.Out(0).Implements(errorType):
|
||||
return nil
|
||||
case mt.Type.NumOut() == 2 && !mt.Type.Out(1).Implements(errorType):
|
||||
return nil
|
||||
}
|
||||
|
||||
fn := mt.Func
|
||||
hasErrOut := mt.Type.NumOut() == 2
|
||||
isPtr := elemType.Kind() == reflect.Pointer
|
||||
|
||||
var callArgs []reflect.Value
|
||||
if needsCtx {
|
||||
callArgs = make([]reflect.Value, 2)
|
||||
callArgs[1] = ctxv
|
||||
} else {
|
||||
callArgs = make([]reflect.Value, 1)
|
||||
}
|
||||
|
||||
return func(v reflect.Value) (reflect.Value, error) {
|
||||
if isPtr && v.IsNil() {
|
||||
return zero, nil
|
||||
}
|
||||
recv := v
|
||||
if !isPtr && !hreflect.IsInterfaceOrPointer(recv.Kind()) && recv.CanAddr() {
|
||||
recv = recv.Addr()
|
||||
}
|
||||
callArgs[0] = recv
|
||||
res := fn.Call(callArgs)
|
||||
if hasErrOut && !res[1].IsNil() {
|
||||
return zero, res[1].Interface().(error)
|
||||
}
|
||||
return res[0], nil
|
||||
}
|
||||
}
|
||||
|
||||
// newInterfaceMethodResolver returns a method resolver or nil if not possible, in which the caller should fall back to evaluateSubElem.
|
||||
func (ns *Namespace) newInterfaceMethodResolver(ctxv reflect.Value, ifaceType reflect.Type, name string) elemResolver {
|
||||
mt, ok := ifaceType.MethodByName(name)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// For interface methods, Type does not include the receiver.
|
||||
mType := mt.Type
|
||||
numIn := mType.NumIn()
|
||||
maxNumIn := 0
|
||||
needsCtx := numIn > 0 && hreflect.IsContextType(mType.In(0))
|
||||
if needsCtx {
|
||||
maxNumIn = 1
|
||||
}
|
||||
|
||||
switch {
|
||||
case mType.NumIn() > maxNumIn:
|
||||
return nil
|
||||
case mType.NumOut() == 0:
|
||||
return nil
|
||||
case mType.NumOut() > 2:
|
||||
return nil
|
||||
case mType.NumOut() == 1 && mType.Out(0).Implements(errorType):
|
||||
return nil
|
||||
case mType.NumOut() == 2 && !mType.Out(1).Implements(errorType):
|
||||
return nil
|
||||
}
|
||||
|
||||
index := mt.Index
|
||||
hasErrOut := mType.NumOut() == 2
|
||||
|
||||
var callArgs []reflect.Value
|
||||
if needsCtx {
|
||||
callArgs = []reflect.Value{ctxv}
|
||||
}
|
||||
|
||||
return func(v reflect.Value) (reflect.Value, error) {
|
||||
if v.IsNil() {
|
||||
return zero, nil
|
||||
}
|
||||
res := v.Method(index).Call(callArgs)
|
||||
if hasErrOut && !res[1].IsNil() {
|
||||
return zero, res[1].Interface().(error)
|
||||
}
|
||||
return res[0], nil
|
||||
}
|
||||
}
|
||||
|
||||
// checkWhereArray handles the where-matching logic when the seqv value is an
|
||||
// Array or Slice.
|
||||
func (ns *Namespace) checkWhereArray(ctxv, seqv, kv, mv reflect.Value, path []string, op string) (any, error) {
|
||||
rv := reflect.MakeSlice(seqv.Type(), 0, 0)
|
||||
|
||||
var resolve elemResolver
|
||||
if kv.Kind() == reflect.String && len(path) > 0 {
|
||||
resolve = ns.newElemResolver(ctxv, seqv.Type().Elem(), path)
|
||||
}
|
||||
|
||||
for i := range seqv.Len() {
|
||||
var vvv reflect.Value
|
||||
rvv := seqv.Index(i)
|
||||
|
||||
if kv.Kind() == reflect.String {
|
||||
if resolve != nil {
|
||||
var err error
|
||||
vvv, err = resolve(rvv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if kv.Kind() == reflect.String {
|
||||
if params, ok := rvv.Interface().(hmaps.Params); ok {
|
||||
vvv = reflect.ValueOf(params.GetNested(path...))
|
||||
} else {
|
||||
@@ -416,7 +619,6 @@ func (ns *Namespace) checkWhereArray(ctxv, seqv, kv, mv reflect.Value, path []st
|
||||
|
||||
if i < len(path)-1 && vvv.IsValid() {
|
||||
if params, ok := vvv.Interface().(hmaps.Params); ok {
|
||||
// The current path element is the map itself, .Params.
|
||||
vvv = reflect.ValueOf(params.GetNested(path[i+1:]...))
|
||||
break
|
||||
}
|
||||
|
||||
@@ -904,46 +904,6 @@ func BenchmarkWhereOps(b *testing.B) {
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkWhereMap(b *testing.B) {
|
||||
ns := newNs()
|
||||
seqString := map[string]string{}
|
||||
seqAny := map[string]any{}
|
||||
seqInt := map[string]int{}
|
||||
|
||||
for i := range 1000 {
|
||||
seqString[fmt.Sprintf("key%d", i)] = "value"
|
||||
seqAny[fmt.Sprintf("key%d", i)] = "value"
|
||||
seqInt[fmt.Sprintf("key%d", i)] = i
|
||||
}
|
||||
|
||||
b.Run("String", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
_, err := ns.Where(context.Background(), seqString, "key", "eq", "value")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Int", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
_, err := ns.Where(context.Background(), seqAny, "key", "eq", 42)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Any", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
_, err := ns.Where(context.Background(), seqAny, "key", "eq", "value")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkWhereSliceOfStructPointersWithMethod(b *testing.B) {
|
||||
// TstRv2
|
||||
ns := newNs()
|
||||
|
||||
+24
-16
@@ -275,6 +275,13 @@ func (ns *Namespace) compareGetWithCollator(collator *langs.Collator, a any, b a
|
||||
}
|
||||
}
|
||||
|
||||
// Fast path: both values are plain strings.
|
||||
if as, aOk := a.(string); aOk {
|
||||
if bs, bOk := b.(string); bOk {
|
||||
return ns.compareTwoStrings(collator, as, bs)
|
||||
}
|
||||
}
|
||||
|
||||
var left, right float64
|
||||
var leftStr, rightStr *string
|
||||
av := reflect.ValueOf(a)
|
||||
@@ -350,33 +357,34 @@ func (ns *Namespace) compareGetWithCollator(collator *langs.Collator, a any, b a
|
||||
}
|
||||
}
|
||||
|
||||
if (ns.caseInsensitive || collator != nil) && leftStr != nil && rightStr != nil {
|
||||
if leftStr != nil && rightStr != nil {
|
||||
return ns.compareTwoStrings(collator, *leftStr, *rightStr)
|
||||
}
|
||||
|
||||
return left, right
|
||||
}
|
||||
|
||||
func (ns *Namespace) compareTwoStrings(collator *langs.Collator, a, b string) (float64, float64) {
|
||||
if ns.caseInsensitive || collator != nil {
|
||||
var c int
|
||||
if collator != nil {
|
||||
c = collator.CompareStrings(*leftStr, *rightStr)
|
||||
c = collator.CompareStrings(a, b)
|
||||
} else {
|
||||
c = compare.Strings(*leftStr, *rightStr)
|
||||
c = compare.Strings(a, b)
|
||||
}
|
||||
if c < 0 {
|
||||
return 0, 1
|
||||
} else if c > 0 {
|
||||
return 1, 0
|
||||
} else {
|
||||
return 0, 0
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case leftStr == nil || rightStr == nil:
|
||||
case *leftStr < *rightStr:
|
||||
return 0, 1
|
||||
case *leftStr > *rightStr:
|
||||
return 1, 0
|
||||
default:
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
return left, right
|
||||
if a < b {
|
||||
return 0, 1
|
||||
} else if a > b {
|
||||
return 1, 0
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
func (ns *Namespace) toTimeUnix(v reflect.Value) int64 {
|
||||
|
||||
Reference in New Issue
Block a user