Most Go benchmark functions do
for i := 0; i < b.N; i++ {
f()
}
The testing documentation has this example:
func BenchmarkBigLen(b *testing.B) {
big := NewBig()
b.ResetTimer()
for i := 0; i < b.N; i++ {
big.Len()
}
}
and in the standard library there are many similar benchmarks such as
this one from encoding/base64:
func BenchmarkEncodeToString(b *testing.B) {
data := make([]byte, 8192)
b.SetBytes(int64(len(data)))
for i := 0; i < b.N; i++ {
StdEncoding.EncodeToString(data)
}
}
In all of these, the result of the function being benchmarked is
unused, so a sufficiently smart compiler could turn the function call
into a no-op. (It seems that the compiler doesn't do this today,
though.)
To protect against this, it is common to introduce a package-scoped
sink variable to which the result is assigned. Here's an example from
math/big:
var sink string
func BenchmarkDecimalConversion(b *testing.B) {
for i := 0; i < b.N; i++ {
for shift := -100; shift <= +100; shift++ {
var d decimal
d.init(natOne, shift)
sink = d.String()
}
}
}
Assuming that a package-scoped sink var is the best way to write
benchmarks, I have two questions:
- Should we document this recommendation? Existing benchmark examples
(as well as the
https://blog.golang.org/subtests blog post) don't use
a sink and as far as I know no official Go documentation points out
this pitfall of writing benchmarks.
- Should go vet report that a function being benchmarked doesn't
assign its result in some blessed way? If such function calls can be
eliminated by the compiler, I don't think the benchmark code can be
considered "correct".
But is a package-scoped sink var even the way to go? As a non-expert
in compilers, I don't see why the compiler can't notice that the sink
var is write-only and replace the assignments with '_ =', at which
point it could eliminate any side-effect-free function calls on the
RHS of those assignments. So it seems to me that by using the
package-scoped sink vars, the benchmark author is implicitly saying "I
think the compiler is likely to become smart enough to do A, but never
smart enough to do B."
Here are two straw man alternatives to package-scoped sink vars:
- Document that the testing package ensures that functions called
inside the benchmark loop are not eliminated. Today we wouldn't have
to make any changes, and we could figure it out later once we
introduce the optimization. (One way would be to have `go test`
rewrite benchmark code to use sinks or other mechanisms.) This idea is
from dominikh.
- Similar to runtime.KeepAlive, add a function like runtime.Use or
testing.Use which ensures that a result will always be used. Today it
can be a no-op. Document that benchmark results should always be
marked as used using this function. (Or can runtime.KeepAlive be
co-opted for this purpose, even?)
If we do introduce an optimization that can eliminate benchmarked
functions, it will break many benchmarks in the standard library and
in the wild, so I think it's worth deciding on an official
recommendation sooner rather than later. If we decide that people need
to use package-scoped sink vars or runtime.Use or something else to
write correct benchmarks, it would be good to give everyone a few
cycles to fix their code before the optimization goes in.
-Caleb