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.

9
  • 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? Commented Jan 5, 2015 at 22:21
  • Yes, I tested it on a variadic function which took *ints 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. Commented Jan 6, 2015 at 0: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. Commented Jan 6, 2015 at 0:24
  • Still, super odd and sort of unfortunate, and may be worth posting reduction to golang-nuts. Commented Jan 6, 2015 at 0:26
  • 5
    Re: "Go can't trivially prove Printf doesn't stash inputs somewhere": More precisely: Go's escape analysis 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". Commented Jan 6, 2015 at 8:07

1 Answer 1

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
    }
}
Sign up to request clarification or add additional context in comments.

2 Comments

ruakh's comment above 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.
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 ... {)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.