I'm currently working on some performance sensitive code in Go. At some point, I have a particularly tight inner loop that does three things in a row:
Get some pointers to the data. In the case of a rare error, one or more of these pointers may be nil .
Check if this error has occurred, and register the error, if any.
Work with data stored in pointers.
Below is a toy program with the same structure (although pointers can never really be anything).
package main import ( "math/rand" "fmt" ) const BigScaryNumber = 1<<25 func DoWork() { sum := 0 for i := 0; i < BigScaryNumber; i++ {
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 delete 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 called, I investigated how the print statement somehow made the pointers stand out on the heap instead of the stack. Running the compiler with the -m flag in the source 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
doing this in a program without print expressions 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 unused fmt.Printf() causes heap allocation, which greatly affects performance. I can get the same behavior by replacing fmt.Printf() with a variable function that does nothing and accepts *int as parameters instead of interface{} s:
func VarArgsError(ptrs ...*int) { panic("An error has occurred.") }
I think this behavior is due to the fact that Go allocates heaps of pointers when they are placed in a slice (although I'm not sure if this is the actual behavior of the escape code analysis procedures, I donโt see how safe it is to be able to do otherwise )
There are two goals in this question: firstly, I want to know if my analysis of the situation is correct, since I really donโt understand how the Go escape analysis analysis works. Secondly, I need suggestions for maintaining the behavior of the original program without causing unnecessary distributions. My best guess is to wrap the Copy() function around the pointers before passing them to 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 } }
Although this gives me the same performance as the no print statement argument, this is weird, not what I want to rewrite for each type of variable, and then wrap the entire error registration code.