I'm currently working on some performance sensitive code in Go. At one point I have a particularly tight inner loop which does three things in succession:
Obtain several pointers to data. In the event of a rare error, one or more of these pointers might be nil
.
Check whether this error has occurred, and log an error if it has.
Do work with the data stored in the pointers.
Shown below is a toy program with the same structure (although the pointers can never actually be nil).
package main
import (
"math/rand"
"fmt"
)
const BigScaryNumber = 1<<25
func DoWork() {
sum := 0
for i := 0; i < BigScaryNumber; i++ {
// Generate pointers.
n1, n2 := rand.Intn(20), rand.Intn(20)
ptr1, ptr2 := &n1, &n2
// Check if pointers are nil.
if ptr1 == nil || ptr2 == nil {
fmt.Printf("Pointers %v %v contain a nil.\n", ptr1, ptr2)
break
}
// Do work with pointer contents.
sum += *ptr1 + *ptr2
}
}
func main() {
DoWork()
}
When I run this on my machine, I get the following:
$ go build alloc.go && time ./alloc
real 0m5.466s
user 0m5.458s
sys 0m0.015s
However, if I remove the print statement, I get the following:
$ go build alloc_no_print.go && time ./alloc_no_print
real 0m4.070s
user 0m4.063s
sys 0m0.008s
Since the print statement is never actually called, I investigated whether the print statement was somehow causing the pointers to be allocated on the heap instead of the stack. Running the compiler with the -m
flag on the original program gives:
$ go build -gcflags=-m alloc.go
# command-line-arguments
./alloc.go:14: moved to heap: n1
./alloc.go:15: &n1 escapes to heap
./alloc.go:14: moved to heap: n2
./alloc.go:15: &n2 escapes to heap
./alloc.go:19: DoWork ... argument does not escape
while doing this on a print statement-less program gives
$ go build -gcflags=-m alloc_no_print.go
# command-line-arguments
./alloc_no_print.go:14: DoWork &n1 does not escape
./alloc_no_print.go:14: DoWork &n2 does not escape
confirming that even an unused fmt.Printf()
is causing heap allocations which have a very real effect on performance. I can get the same behavior by replacing fmt.Printf()
with a variadic function which does nothing and takes *int
s as parameters instead of interface{}
s:
func VarArgsError(ptrs ...*int) {
panic("An error has occurred.")
}
I think this behavior is because Go allocates pointers on the heap whenever they are placed in a slice (although I'm not sure that this is the actual behavior of the escape analysis routines, I don't see how it would safely be able to do otherwise).
There are two purposes to this question: first, I want to know if my analysis of the situation is correct, since I don't really understand how Go's escape analysis works. And second, I wanted suggestions for maintaining the behavior of the original program without causing unneeded allocations. My best guess is to wrap a Copy()
function around the pointers prior to passing them into the print statement:
fmt.Printf("Pointers %v %v contain a nil.", Copy(ptr1), Copy(ptr2))
where Copy()
is defined as
func Copy(ptr *int) *int {
if ptr == nil {
return nil
} else {
n := *ptr
return &n
}
}
While this gives me the same performance as the no print statement case, it's weird and not the sort of thing I want to rewrite for every variable type and then wrap around all of my error logging code.
From Go FAQ,
In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.
When the pointers are passed to a function, I think it fails the second part of escape analysis. For example, the function may assign the pointer to a global variable in its package which lives longer than the current stack. I don't think the current compiler does such deep escape analysis.
One way to avoid the cost of allocation would be to move the allocation outside the loop and reassign the value to allocated memory inside the loop.
func DoWork() {
sum := 0
n1, n2 := new(int), new(int)
for i := 0; i < BigScaryNumber; i++ {
*n1, *n2 = rand.Intn(20), rand.Intn(20)
ptr1, ptr2 := n1, n2
// Check if pointers are nil.
if ptr1 == nil || ptr2 == nil {
fmt.Printf("Pointers %v %v contain a nil.\n", n1, n2)
break
}
// Do work with pointer contents.
sum += *ptr1 + *ptr2
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With