Zhandos Zhandarbek would like Sean Liao and Alan Donovan to review this change.
text/template: avoid reflect.Value.Call for common builtins
CL 835445 added a fast path in safeCall for func(...any) string. The
other builtins still go through reflect.Value.Call: eq, ne, lt, le, gt,
ge, index, slice, len, not and printf. Comparisons and index appear in
almost every real template, typically as {{if eq .X "y"}}.
Check for the signatures of these builtins in safeCall and call the
function directly. Arguments of type reflect.Value are unwrapped with
reflect.TypeAssert, which does not allocate. User-defined functions
with the same signatures take the same path. The checks sit inside the
existing recover, so a panic is still reported as an error. Functions
with other signatures are unaffected (userFunc below).
Old and new binaries run alternately, 10 runs each, on an Apple M4 Pro:
pkg: text/template
│ old.txt │ new.txt │
│ sec/op │ sec/op vs base │
ExecuteBuiltins/eq-12 548.2n ± 2% 286.4n ± 2% -47.77% (p=0.000 n=10)
ExecuteBuiltins/eqMulti-12 666.2n ± 2% 391.0n ± 4% -41.32% (p=0.000 n=10)
ExecuteBuiltins/lt-12 476.1n ± 2% 262.4n ± 3% -44.88% (p=0.000 n=10)
ExecuteBuiltins/not-12 362.1n ± 2% 210.0n ± 2% -42.00% (p=0.000 n=10)
ExecuteBuiltins/len-12 434.8n ± 3% 245.6n ± 3% -43.53% (p=0.000 n=10)
ExecuteBuiltins/index-12 617.6n ± 3% 356.4n ± 5% -42.31% (p=0.000 n=10)
ExecuteBuiltins/printf-12 655.4n ± 3% 439.9n ± 4% -32.88% (p=0.000 n=10)
│ old.txt │ new.txt │
│ allocs/op │ allocs/op vs base │
ExecuteBuiltins/eq-12 14.000 ± 0% 9.000 ± 0% -35.71% (p=0.000 n=10)
ExecuteBuiltins/eqMulti-12 18.00 ± 0% 13.00 ± 0% -27.78% (p=0.000 n=10)
ExecuteBuiltins/lt-12 10.000 ± 0% 7.000 ± 0% -30.00% (p=0.000 n=10)
ExecuteBuiltins/not-12 7.000 ± 0% 5.000 ± 0% -28.57% (p=0.000 n=10)
ExecuteBuiltins/len-12 8.000 ± 0% 5.000 ± 0% -37.50% (p=0.000 n=10)
ExecuteBuiltins/index-12 15.00 ± 0% 11.00 ± 0% -26.67% (p=0.000 n=10)
ExecuteBuiltins/printf-12 14.00 ± 0% 11.00 ± 0% -21.43% (p=0.000 n=10)
pkg: text/template
│ old.txt │ new.txt │
│ sec/op │ sec/op vs base │
ExecuteBuiltins/userFunc-12 372.7n ± 2% 371.6n ± 3% ~ (p=0.955 n=10)
Updates #81610
diff --git a/src/text/template/exec_test.go b/src/text/template/exec_test.go
index 48895db..53359c6 100644
--- a/src/text/template/exec_test.go
+++ b/src/text/template/exec_test.go
@@ -1782,6 +1782,12 @@
"doPanicVariadic": func(...any) string {
panic("custom panic string")
},
+ "doPanicCompare": func(reflect.Value, reflect.Value) (bool, error) {
+ panic("custom panic string")
+ },
+ "doPanicFormat": func(string, ...any) string {
+ panic("custom panic string")
+ },
}
tests := []struct {
name string
@@ -1810,6 +1816,16 @@
`template: t:1:6: executing "t" at <doPanicVariadic>: error calling doPanicVariadic: custom panic string`,
},
{
+ "compare-like func call panics",
+ "{{doPanicCompare 1 2}}", (*T)(nil),
+ `template: t:1:2: executing "t" at <doPanicCompare 1 2>: error calling doPanicCompare: custom panic string`,
+ },
+ {
+ "printf-like func call panics",
+ `{{2 | doPanicFormat "%d"}}`, (*T)(nil),
+ `template: t:1:6: executing "t" at <doPanicFormat "%d">: error calling doPanicFormat: custom panic string`,
+ },
+ {
"direct method call panics",
"{{.GetU}}", (*T)(nil),
`template: t:1:2: executing "t" at <.GetU>: error calling GetU: runtime error: invalid memory address or nil pointer dereference`,
@@ -2043,3 +2059,90 @@
}
}
}
+
+func BenchmarkExecuteBuiltins(b *testing.B) {
+ data := map[string]any{
+ "S": "active",
+ "N": 3,
+ "L": []int{1, 2, 3},
+ "M": map[string]string{"k": "v"},
+ }
+ tmpls := []struct{ name, text string }{
+ {"eq", `{{if eq .S "active"}}y{{end}}`},
+ {"eqMulti", `{{if eq .S "a" "b" "active"}}y{{end}}`},
+ {"lt", `{{if lt .N 5}}y{{end}}`},
+ {"not", `{{if not .S}}y{{end}}`},
+ {"len", `{{len .L}}`},
+ {"index", `{{index .M "k"}}`},
+ {"printf", `{{printf "%s-%d" .S .N}}`},
+ {"userFunc", `{{double .N}}`},
+ }
+ for _, tt := range tmpls {
+ tmpl := Must(New("t").Funcs(FuncMap{"double": func(n int) int { return 2 * n }}).Parse(tt.text))
+ b.Run(tt.name, func(b *testing.B) {
+ b.ReportAllocs()
+ for b.Loop() {
+ if err := tmpl.Execute(io.Discard, data); err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+ }
+}
+
+func TestExecuteBuiltinSignatureFuncs(t *testing.T) {
+ funcs := FuncMap{
+ "same": func(a, b reflect.Value) (bool, error) {
+ if a.Kind() != b.Kind() {
+ return false, fmt.Errorf("kinds differ: %s, %s", a.Kind(), b.Kind())
+ }
+ return a.Interface() == b.Interface(), nil
+ },
+ "anyOf": func(x reflect.Value, ys ...reflect.Value) (bool, error) {
+ for _, y := range ys {
+ if x.Interface() == y.Interface() {
+ return true, nil
+ }
+ }
+ return false, nil
+ },
+ "first": func(x reflect.Value, _ ...reflect.Value) (reflect.Value, error) {
+ return x.Index(0), nil
+ },
+ "size": func(x reflect.Value) (int, error) { return x.Len(), nil },
+ "isStr": func(x reflect.Value) bool { return x.Kind() == reflect.String },
+ "fmt": func(format string, args ...any) string { return fmt.Sprintf(format, args...) },
+ }
+ tests := []struct {
+ input, want, wantErr string
+ }{
+ {`{{same 1 1}} {{same "a" "b"}} {{1 | same 1}}`, "true false true", ""},
+ {`{{same 1 "a"}}`, "", "error calling same: kinds differ: int, string"},
+ {`{{anyOf 3 1 2 3}} {{anyOf 3}} {{3 | anyOf 1 2}}`, "true false false", ""},
+ {`{{first .L}} {{.L | first}}`, "x x", ""},
+ {`{{size .L}} {{.L | size}}`, "2 2", ""},
+ {`{{isStr "a"}} {{isStr 1}} {{"a" | isStr}}`, "true false true", ""},
+ {`{{same .RV "s"}} {{isStr .RV}} {{size .RV}}`, "true true 1", ""},
+ {`{{eq nil nil}} {{not nil}}`, "true true", ""},
+ {`{{fmt "%s=%d" "a" 1}} {{1 | fmt "%d"}} {{"%%" | fmt}}`, "a=1 1 %", ""},
+ }
+ for _, tt := range tests {
+ tmpl := Must(New("t").Funcs(funcs).Parse(tt.input))
+ var b strings.Builder
+ err := tmpl.Execute(&b, struct {
+ L []string
+ RV reflect.Value
+ }{[]string{"x", "y"}, reflect.ValueOf("s")})
+ if tt.wantErr != "" {
+ if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
+ t.Errorf("%s: got error %v, want %q", tt.input, err, tt.wantErr)
+ }
+ continue
+ }
+ if err != nil {
+ t.Errorf("%s: unexpected error: %v", tt.input, err)
+ } else if b.String() != tt.want {
+ t.Errorf("%s: got %q, want %q", tt.input, b.String(), tt.want)
+ }
+ }
+}
diff --git a/src/text/template/funcs.go b/src/text/template/funcs.go
index 9985eab..9e2fb71 100644
--- a/src/text/template/funcs.go
+++ b/src/text/template/funcs.go
@@ -373,6 +373,33 @@
}
return reflect.ValueOf(f(anyArgs...)), nil
}
+ // Fast paths for the signatures of the other builtins.
+ if f, ok := reflect.TypeAssert[func(reflect.Value, reflect.Value) (bool, error)](fun); ok {
+ r, err := f(unwrapArg(args[0]), unwrapArg(args[1]))
+ return reflect.ValueOf(r), err
+ }
+ if f, ok := reflect.TypeAssert[func(reflect.Value, ...reflect.Value) (bool, error)](fun); ok {
+ r, err := f(unwrapArg(args[0]), unwrapArgs(args[1:])...)
+ return reflect.ValueOf(r), err
+ }
+ if f, ok := reflect.TypeAssert[func(reflect.Value, ...reflect.Value) (reflect.Value, error)](fun); ok {
+ r, err := f(unwrapArg(args[0]), unwrapArgs(args[1:])...)
+ return reflect.ValueOf(r), err
+ }
+ if f, ok := reflect.TypeAssert[func(reflect.Value) (int, error)](fun); ok {
+ r, err := f(unwrapArg(args[0]))
+ return reflect.ValueOf(r), err
+ }
+ if f, ok := reflect.TypeAssert[func(reflect.Value) bool](fun); ok {
+ return reflect.ValueOf(f(unwrapArg(args[0]))), nil
+ }
+ if f, ok := reflect.TypeAssert[func(string, ...any) string](fun); ok {
+ anyArgs := make([]any, len(args)-1)
+ for i, arg := range args[1:] {
+ anyArgs[i] = arg.Interface()
+ }
+ return reflect.ValueOf(f(args[0].String(), anyArgs...)), nil
+ }
ret := fun.Call(args)
if len(ret) == 2 && !ret[1].IsNil() {
return ret[0], ret[1].Interface().(error)
@@ -380,6 +407,24 @@
return ret[0], nil
}
+// unwrapArg returns the reflect.Value that evalCall wraps in another
+// reflect.Value for parameters of type reflect.Value.
+func unwrapArg(arg reflect.Value) reflect.Value {
+ v, ok := reflect.TypeAssert[reflect.Value](arg)
+ if !ok {
+ panic("text/template: argument is not a wrapped reflect.Value")
+ }
+ return v
+}
+
+func unwrapArgs(args []reflect.Value) []reflect.Value {
+ vs := make([]reflect.Value, len(args))
+ for i, arg := range args {
+ vs[i] = unwrapArg(arg)
+ }
+ return vs
+}
+
// Boolean logic.
func truth(arg reflect.Value) bool {
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Congratulations on opening your first change. Thank you for your contribution!
Next steps:
A maintainer will review your change and provide feedback. See
https://go.dev/doc/contribute#review for more info and tips to get your
patch through code review.
Most changes in the Go project go through a few rounds of revision. This can be
surprising to people new to the project. The careful, iterative review process
is our way of helping mentor contributors and ensuring that their contributions
have a lasting impact.
During May-July and Nov-Jan the Go project is in a code freeze, during which
little code gets reviewed or merged. If a reviewer responds with a comment like
R=go1.11 or adds a tag like "wait-release", it means that this CL will be
reviewed as part of the next development cycle. See https://go.dev/s/release
for more details.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
PS2: added an html/template page benchmark (-37% time, -38% allocs) and switched to a type switch so functions with other signatures are not slowed down. PS1 was measured against the wrong baseline and missed a ~3% overhead there.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Are all of these needed? I would guess most builtins are pretty rarely used in production and not worth optimizing.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Are all of these needed? I would guess most builtins are pretty rarely used in production and not worth optimizing.
Fair enough. I grepped bitnami/charts (~3300 templates, plain
text/template + sprig) to see what's actually used:
printf 2.5%, not 2.4%, eq 2.2%, ne 0.5% of all actions
index 0.1%, len/lt/le/gt/ge/slice under 0.1%
and/or are already handled separately in evalCall.
So I dropped index, slice and len in PS3. The page benchmark is still
-38%
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Code-Review | +1 |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
text/template: call eq, not, printf and comparisons directly
CL 835445 added a fast path in safeCall for func(...any) string.
eq, ne, lt, le, gt, ge, not and printf still go through
reflect.Value.Call. In bitnami/charts, printf, not, eq and ne make up
about 7.6% of all template actions; the other builtins are rare.
Switch on the function type in safeCall and call these signatures
directly. Functions with other signatures pay one type comparison
(userFunc below). The call is still inside the existing recover.
Old and new binaries run alternately on an Apple M4 Pro:
pkg: html/template
│ old │ new │
│ sec/op │ sec/op vs base │
EscapedExecuteBuiltinsPage-12 35.53µ ± 2% 21.95µ ± 2% -38.24% (p=0.000 n=15)
│ old │ new │
│ allocs/op │ allocs/op vs base │
EscapedExecuteBuiltinsPage-12 564.0 ± 0% 364.0 ± 0% -35.46% (p=0.000 n=15)
pkg: text/template
│ old │ new │
│ sec/op │ sec/op vs base │
ExecuteBuiltins/eq-12 548.6n ± 2% 275.9n ± 4% -49.71% (p=0.000 n=15)
ExecuteBuiltins/eqMulti-12 672.5n ± 3% 373.0n ± 2% -44.54% (p=0.000 n=15)
ExecuteBuiltins/lt-12 476.1n ± 3% 265.4n ± 3% -44.26% (p=0.000 n=15)
ExecuteBuiltins/not-12 362.3n ± 1% 198.2n ± 2% -45.29% (p=0.000 n=15)
ExecuteBuiltins/printf-12 651.2n ± 2% 418.8n ± 3% -35.69% (p=0.000 n=15)
ExecuteBuiltins/userFunc-12 361.7n ± 3% 356.6n ± 3% ~ (p=0.721 n=15)
geomean 496.5n 305.2n -38.53%
│ old │ new │
│ allocs/op │ allocs/op vs base │
ExecuteBuiltins/eq-12 14.000 ± 0% 9.000 ± 0% -35.71% (p=0.000 n=15)
ExecuteBuiltins/eqMulti-12 18.00 ± 0% 13.00 ± 0% -27.78% (p=0.000 n=15)
ExecuteBuiltins/lt-12 10.000 ± 0% 7.000 ± 0% -30.00% (p=0.000 n=15)
ExecuteBuiltins/not-12 7.000 ± 0% 5.000 ± 0% -28.57% (p=0.000 n=15)
ExecuteBuiltins/printf-12 14.00 ± 0% 11.00 ± 0% -21.43% (p=0.000 n=15)
ExecuteBuiltins/userFunc-12 6.000 ± 0% 6.000 ± 0% ~ (p=1.000 n=15)
Updates #81610
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |