9

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:

  1. Obtain several pointers to data. In the event of a rare error, one or more of these pointers might be nil.

  2. Check whether this error has occurred, and log an error if it has.

  3. 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 *ints 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.

ruakh
  • 175,680
  • 26
  • 273
  • 307
mansfield
  • 195
  • 6
  • Well, for starters, the `fmt` package heavily utilizes reflection to get all of the fancy struct printing it does. That can be a bottleneck if you're really shooting for performance. I realize its not even being called - but that's something else to think about. May I ask, what happens if you write your own variadic function that accepts arguments that are __NOT__ of type `interface{}`? Do you see the same issues? – Simon Whitehead Jan 05 '15 at 22:21
  • Yes, I tested it on a variadic function which took `*int`s as arguments, but forgot to specify that or to include the source (which I have now done). The results are the same as with `Printf()`. Also, for the reasons you have mentioned, I generally do not use the `fmt` package in sections which are critical to performance. Although it is certainly a good thing to note. – mansfield Jan 06 '15 at 00:01
  • Oh, only marginally prettier, but another option: `ptr1, ptr2 := ptr1, ptr2` inside the if block. Unless the compiler optimizes that away, now it's two variables declared inside the 'if' that escape, which might be analogous to the temporary returned from `Copy` created inside the `if` escaping. – twotwotwo Jan 06 '15 at 00:24
  • Still, super odd and sort of unfortunate, and may be worth posting reduction to `golang-nuts`. – twotwotwo Jan 06 '15 at 00:26
  • Slightly curious if explicitly creating the slice at the callsite, as in `fmt.Printf("%v %v", []interface{}{ptr1, ptr2}...)` helps--doubt it (seems like that must be how Go looks at the existing printf), arguably uglier than `a, b := a, b`, but, you know, for science. – twotwotwo Jan 06 '15 at 00:30
  • Yup, nope, none of that worked (http://play.golang.org/p/vFFq_ibE-D). Whee, I've got nothin'. – twotwotwo Jan 06 '15 at 00:38
  • Looking again I just realized `Copy` is actually allocating a *new integer* that escapes in place of the original. That makes me think it's just that any pointer passed passed to `Printf` is assumed to escape (i.e., Go can't trivially prove `Printf` doesn't stash inputs somewhere). That makes me pessimistic about finding a simple syntactical change that avoids this as long as long as `ptr` (not `ptr==nil` or `*ptr`) is being passed into `Printf`. – twotwotwo Jan 06 '15 at 02:52
  • 4
    Re: "Go can't trivially prove Printf doesn't stash inputs somewhere": More precisely: [Go's escape analysis](http://golang.org/src/cmd/gc/esc.c) really only applies to function parameters and local variables. Since `Printf`'s formal parameter is a slice, the escape analysis only verifies that the slice doesn't escape. It doesn't bother to check whether the elements of the slice (the actual arguments) could escape. This is what mansfield was getting at with his/her supposition that "Go allocates pointers on the heap whenever they are placed in a slice". – ruakh Jan 06 '15 at 08:07
  • Yeah. Pretty rough that there's no obvious non-escaping alt. here--oh well. – twotwotwo Jan 06 '15 at 20:57

1 Answers1

1

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
    }
}
Chandra Sekar
  • 10,683
  • 3
  • 39
  • 54
  • [ruakh's comment above](http://stackoverflow.com/questions/27788813/variadic-functions-causing-unnecessary-heap-allocations-in-go#comment43998000_27788813) is more accurate as to the reason why. Anyhow your solution works IF the variadic function called does not change the values or store the pointers for later use, so it's fine if the OP just does a Printf should an error occur. – wldsvc Apr 15 '15 at 22:49
  • Scratch what I said, your code forces a copy of the incoming data (n1 and n2) at every iteration of the loop, which is suboptimal in this case. He's probably dealing with pointers to structures much bigger than an int. His only solution is to copy the data only in the escaping block (`if ptr1 == nil ... {`) – wldsvc Apr 19 '15 at 21:16