Description
Take a look at https://go.dev/play/p/mPqA0RaybuQ.
On go version devel go1.19-9068c6844d Thu Jun 16 21:58:40 2022 +0000 linux/amd64
I get the following benchmark results:
name time/op
ConditionalDebugf/false-16 69.4ns ± 3%
ConditionalDebugf/true-16 280ns ± 0%
InlinedDebugf/false-16 0.53ns ± 1%
InlinedDebugf/true-16 281ns ± 4%
name alloc/op
ConditionalDebugf/false-16 24.0B ± 0%
ConditionalDebugf/true-16 40.0B ± 0%
InlinedDebugf/false-16 0.00B
InlinedDebugf/true-16 40.0B ± 0%
name allocs/op
ConditionalDebugf/false-16 2.00 ± 0%
ConditionalDebugf/true-16 3.00 ± 0%
InlinedDebugf/false-16 0.00
InlinedDebugf/true-16 3.00 ± 0%
First, note that both debugf functions are inlined:
$ go test -gcflags='-m' |& grep Debugf
./go_test.go:12:6: can inline conditionalDebugf
./go_test.go:18:6: can inline inlinedDebugf
./go_test.go:30:22: inlining call to conditionalDebugf
./go_test.go:45:19: inlining call to inlinedDebugf
And, in the true
sub-benchmarks where debug logging is enabled and we actually call Sprintf
, we can see three allocations - two corresponding to the two interface{}
parameters, as the string
and int
variables escape to the heap via the interface, and one for the actual call to Sprintf
constructing a string.
So far so good. However, when debug logging is disabled, I want to avoid those allocations, because this debug logging happens in a tight loop that runs millions of times in a program, so I want to avoid millions of allocations made in a very short amount of time.
This works fine for the "manually inlined" case where I wrap the call to Debugf with a conditional. Note the zero allocations on InlinedDebugf/false
. However, that means that every time I want to use Debugf (a dozen times in said program), I need three lines rather than one, and it's pretty repetitive and verbose.
One good alternative is to move the conditional inside the Debugf function - and since it's still inlined, it should be the same result. However, ConditionalDebugf/false
allocates twice, presumably because the two arguments do still allocate, as if I was actually calling Sprintf
when in fact I am not.
It seems to me like the two benchmarks should behave the same; the compiler's escape analysis and inliner should work well enough together to detect this scenario.
Note that my real use case involves log.Printf
, which is a no-op when one has called log.SetOutput(io.Discard)
. The benchmark above is a simplification which still reproduces the same problem.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status