Skip to content

Commit f922d78

Browse files
authored
perf: zero alloc lower/upper unless changed (#7472)
Similar to how some other built-in functions have been optimized previously, the `lower` and `upper` functions now return the operand as-is when the string wasn't modified by the operation. The impact of this check is negligible when the string is modified, and as the benchmarks demonstrate, quite an improvement for the cases where it isn't. For those, we no longer need to allocate at all. Signed-off-by: Anders Eknert <[email protected]>
1 parent aa2a16f commit f922d78

File tree

2 files changed

+44
-1
lines changed

2 files changed

+44
-1
lines changed

Diff for: v1/topdown/strings.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,14 @@ func builtinUpper(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) e
443443
return err
444444
}
445445

446-
return iter(ast.StringTerm(strings.ToUpper(string(s))))
446+
arg := string(s)
447+
upp := strings.ToUpper(arg)
448+
449+
if arg == upp {
450+
return iter(operands[0])
451+
}
452+
453+
return iter(ast.StringTerm(upp))
447454
}
448455

449456
func builtinSplit(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {

Diff for: v1/topdown/strings_bench_test.go

+36
Original file line numberDiff line numberDiff line change
@@ -227,3 +227,39 @@ func BenchmarkTrimSpace(b *testing.B) {
227227
})
228228
}
229229
}
230+
231+
// Benchmark to demonstrate the performance difference when calling lower with a string
232+
// that is already lowercase vs. one that is not. In the former case, the provided operand
233+
// is returned as-is, while in the latter case a new string is allocated and returned.
234+
// While this tests the 'lower' builtin, the same optimization applies to 'upper'.
235+
//
236+
// BenchmarkLower/not_lowercase-10 5960936 198.6 ns/op 88 B/op 3 allocs/op
237+
// BenchmarkLower/lowercase-10 20954871 57.36 ns/op 0 B/op 0 allocs/op
238+
func BenchmarkLower(b *testing.B) {
239+
bctx := BuiltinContext{}
240+
lower := ast.StringTerm("the quick brown fox jumps over the lazy dog")
241+
cases := []struct {
242+
name string
243+
operands []*ast.Term
244+
}{
245+
{
246+
name: "not lowercase",
247+
operands: []*ast.Term{ast.StringTerm("The Quick Brown Fox Jumps Over The Lazy Dog")},
248+
},
249+
{
250+
name: "lowercase",
251+
operands: []*ast.Term{lower},
252+
},
253+
}
254+
255+
for _, c := range cases {
256+
b.Run(c.name, func(b *testing.B) {
257+
b.ResetTimer()
258+
for range b.N {
259+
if err := builtinLower(bctx, c.operands, eqIter(lower)); err != nil {
260+
b.Fatal(err)
261+
}
262+
}
263+
})
264+
}
265+
}

0 commit comments

Comments
 (0)