-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Description
Our options policy design includes the creation of options using With* functions similar to this:
type Config struct {
IntSlice []int
}
type Option interface {
apply(Config) Config
}
type intSliceOpt struct {
val []int
}
func (o intSliceOpt) apply(c Config) Config {
c.IntSlice = o.val
return c
}
func WithIntSlice(val []int) Option {
return intOpt{val: []val} // Allocated to the heap when returned as an Option.
}This With* pattern requires can1, at a minimum, require 1 heap allocation everytime the function is called due to interface boxing.
This is problematic as these options are sometimes used in "hot path" or performance sensitive call paths.
Originally posted by @dashpole in #7777 (comment)
Benchmark
To evaluate this claim the following benchmark was used:
func BenchmarkWithIntSlice(b *testing.B) {
var opt Option
var c Config
ints := []int{1, 2, 3, 4, 5}
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
opt = WithIntSlice(ints)
c = opt.apply(c)
}
_ = c
}Running this:
> go version
go version go1.25.6 X:nodwarf5 linux/amd64
> go test -bench=BenchmarkWithIntSlice
goos: linux
goarch: amd64
pkg: testing/attropt
cpu: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
BenchmarkWithIntSlice-8 22197412 54.39 ns/op 24 B/op 1 allocs/op
PASS
ok testing/attropt 1.210s
Caveat: not all options are the same
The modern Go compiler looks smart enough to get itself out of certain allocations. Take for instance:
type Config struct {
Int int
}
type Option interface {
apply(Config) Config
}
type intOpt struct {
val int
}
func (o intOpt) apply(c Config) Config {
c.Int = o.val
return c
}
func WithInt(val int) Option {
return intOpt{val: val}
}
func BenchmarkWithInt(b *testing.B) {
var opt Option
var c Config
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
opt = WithInt(n)
c = opt.apply(c)
}
_ = c
}> go test -bench=BenchmarkWithInt$
goos: linux
goarch: amd64
pkg: testing/attropt
cpu: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
BenchmarkWithInt-8 31261698 32.41 ns/op 8 B/op 0 allocs/op
Seen here, there are no allocations for this case.
Looking at the escape analysis for this ...
[...]
[...]: inlining call to WithInt
[...]: intOpt{...} escapes to heap
[...]
WithInt is simple enough that it gets inlined. The intOpt instance still escapes to the heap, but since the opt var is local, that heap is not allocated repeatedly for each iteration.
Footnotes
-
see the following caveats ↩