-1

When I am talking about Go, I am speaking about the gc compiler implementation.

As far as I know, Go performs escape analysis. The following idiom is seen pretty often in Go code:

func NewFoo() *Foo

Escape analysis would notice that Foo escapes NewFoo and allocate Foo on the heap.

This function could also be written as:

func NewFoo(f *Foo)

and would be used like

var f Foo
NewFoo(&f)

In this case, as long as f doesn't escape anywhere else, f could be allocated on the stack.

Now to my actual question.

Would it be possible for the compiler to optimize every foo() *Foo into foo(f *Foo), even possibly over multiple levels where Foo is returned in each?

If not, in what kind of cases does this approach fail?

Thank you in advance.

sqrooty
  • 121
  • 4
  • 1
    It was my impression that Go does inlining before escape analysis. So, are you *sure* NewFoo in your example allocates on the heap? I suspect it is inlined into the caller and if it doesn't escape the caller, it won't be heap allocated. – Zan Lynx Aug 23 '15 at 23:45
  • @Zan No, I am not sure, but now that you mentioned it I looked it up and Go does indeed do inlining. I am sorry, I had looked at three year old SO questions and go-nuts posts, which claimed that gc would allocate data on the heap when using the NewFoo idiom. I'll test this now. – sqrooty Aug 24 '15 at 00:03
  • 1
    Dave C's answer is part of it: reasonably often the constructor gets inlined. When you need to pinch allocations of something, a common pattern is `thing.Reset()` to reuse an empty `Thing` and a `sync.Pool` to manage them. Of course, you can then also use `f.Reset()` in place of `NewFoo(f)` in your construction, but that isn't idiomatic--I'd only do it if it led to a measurable improvement in a critical part of your program (like, a spot where a lot of garbage is generated, in a program where performance needs tuning and collection time is an important factor in that). – twotwotwo Aug 24 '15 at 02:57
  • Though you asked a pretty reasonable q (behavior here isn't obvious), my general advice is to build the functionality you want, then measure and tune the fatty bits only when you have nothing better to do. Life's too precious to spend too much of it sweating processor cycles. :) – twotwotwo Aug 24 '15 at 03:05
  • @two Of course. My issue was that I thought of a particular compiler optimization that could've been possible in Go but didn't know if this was even possible or if this exists. I am usually the first to try to prove myself wrong, but I couldn't come up with cases where the approach fails, and I couldn't find what I was looking for either, so I decided to ask on SO, because someone here might be able to prove me wrong or even point me in the right direction. In the end I still managed to find it myself. – sqrooty Aug 24 '15 at 10:39

2 Answers2

6

(Not quite an answer but too big for a comment.)

From the comments it seems you might be interested in this small example:

package main

type Foo struct {
    i, j, k int
}

func NewFoo() *Foo {
    return &Foo{i: 42}
}

func F1() {
    f := NewFoo()
    f.i++
}

func main() {
    F1()
}

On Go1.5 running go build -gcflags="-m" gives:

./new.go:7: can inline NewFoo
./new.go:11: can inline F1
./new.go:12: inlining call to NewFoo
./new.go:16: can inline main
./new.go:17: inlining call to F1
./new.go:17: inlining call to NewFoo
./new.go:8: &Foo literal escapes to heap
./new.go:12: F1 &Foo literal does not escape
./new.go:17: main &Foo literal does not escape

So it inlines NewFoo into F1 into main (and says that it could further inline main if someone was to call it). Although it does say that in NewFoo itself &Foo escapes, it does not escape when inlined.

The output from go build -gcflags="-m -S" confirms this with a main initializing the object on the stack and not doing any function calls.

Of course this is a very simple example and any complications (e.g. calling fmt.Print with f) could easily cause it to escape. In general, you shouldn't worry about this too much unless profiling has told you that you have a problem area(s) and you are trying to optimize a specific section of code. Idiomatic and readable code should trump optimization.

Also note that using go test -bench -benchmem (or preferably using testing.B's ReportAllocs method) can report on allocations of benchmarked code which can help identify something doing more allocations than expected/desired.

Dave C
  • 7,729
  • 4
  • 49
  • 65
  • I am accepting this answer instead of my own because my question was arguably too broad and more about optimizing away heap allocations in cases like I mentioned instead of being specifically about optimizing it via RVO. – sqrooty Aug 24 '15 at 11:13
2

After doing some more research I found what I was looking for.

What I was describing is apparently called "Return value optimization" and is well doable, which pretty much answers my question about whether this was possible in Go as well.

Further information about this can be found here: What are copy elision and return value optimization?

Community
  • 1
  • 1
sqrooty
  • 121
  • 4