Rules extra allocation in dynamic dispatch

134 views
Skip to first unread message

Peter Mogensen

unread,
May 26, 2016, 9:07:04 AM5/26/16
to golang-nuts
Hi,

I stumbled upon an extra allocation in a benchmark using dynamic
dispatch and I can't really figure out when it occurs and how to avoid it.
At first glance it seems it's when you do dynamic dispatch of a function
taking an interface{} as argument.
But that doesn't seem to be the whole truth.

Here's an example of the allocation I'm taking about:
========================================================

type K interface {
s(*thing, int)
d(*thing, interface{})
}

type thing struct {}

func (t *thing) d(val interface{}) {
}

func (t *thing) s(val int) {
}

type thinger struct {}

func (g *thinger) d(t *thing, val interface{}) {
t.d(val)
}
func (g *thinger) s(t *thing, val int) {
t.s(val)
}

func BenchmarkIfaceSD(b *testing.B) {
var k *thinger
t := &thing{}
k = &thinger{}

for i := 0; i < b.N; i++ {
k.d(t,42)
}
}

func BenchmarkIfaceDS(b *testing.B) {
var k K
t := &thing{}
k = &thinger{}

for i := 0; i < b.N; i++ {
k.s(t,42)
}
}

func BenchmarkIfaceSS(b *testing.B) {
var k *thinger
t := &thing{}
k = &thinger{}

for i := 0; i < b.N; i++ {
k.s(t,42)
}
}

func BenchmarkIfaceDD(b *testing.B) {
var k K
t := &thing{}
k = &thinger{}

for i := 0; i < b.N; i++ {
k.d(t,42)
}
}
========================================

go test -bench Iface -benchmem

BenchmarkIfaceSD-4 100000000 10.9 ns/op 0 B/op 0
allocs/op
BenchmarkIfaceDS-4 1000000000 2.01 ns/op 0 B/op 0
allocs/op
BenchmarkIfaceSS-4 2000000000 0.33 ns/op 0 B/op 0
allocs/op
BenchmarkIfaceDD-4 50000000 29.6 ns/op 8 B/op 1
allocs/op


What's the rules for avoiding that allocation in the last test?

/Peter

Jakob Borg

unread,
May 26, 2016, 9:39:53 AM5/26/16
to Peter Mogensen, golang-nuts
I suspect this is due to escape analysis. In your first tests, the
compiler can see that the second parameter doesn't escape so it can
create an interface{} boxing of 42 on the heap. When calling a method
on an interface it can't see that, so it needs to heap allocate the
interface{} for 42.

You can see this if you compile with "-gcflags -m".

//jb
> --
> You received this message because you are subscribed to the Google Groups
> "golang-nuts" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to golang-nuts...@googlegroups.com.
> For more options, visit https://groups.google.com/d/optout.

Jakob Borg

unread,
May 26, 2016, 9:40:23 AM5/26/16
to Peter Mogensen, golang-nuts
2016-05-26 15:39 GMT+02:00 Jakob Borg <ja...@nym.se>:
> I suspect this is due to escape analysis. In your first tests, the
> compiler can see that the second parameter doesn't escape so it can
> create an interface{} boxing of 42 on the heap. When calling a method

Gnnh. On the *stack* of course.

Peter Mogensen

unread,
May 26, 2016, 9:49:11 AM5/26/16
to golan...@googlegroups.com


On 2016-05-26 15:39, Jakob Borg wrote:
> I suspect this is due to escape analysis.

Seems you are right...

Now... I have some code, which I would say also calls a method on an
interface, which takes an interface as argument... but which don't cost
the extra allocation.

So... it's kind of at the mercy of the skills of the optimizer I guess?

... That there's no really bulletproof way to avoid it without relying
on internal details of the optimizer?


/Peter


Martin Schnabel

unread,
May 26, 2016, 2:19:11 PM5/26/16
to golan...@googlegroups.com
i think the situation you describe may be something like this:

func BenchmarkIfaceDD(b *testing.B) {
        var k K = &thinger{}
        t := &thing{}
        var n interface{} = 42
        
        for i := 0; i < b.N; i++ {
                k.d(t, n)
        }
}

where the argument is already an interface value and does not need to
be boxed again. otherwise the optimiser can never know whether the
argument escapes, unless it does very expensive whole program analysis.

Peter Mogensen

unread,
May 26, 2016, 2:59:49 PM5/26/16
to golan...@googlegroups.com


On 2016-05-26 20:18, Martin Schnabel wrote:
> i think the situation you describe may be something like this:
>
> func BenchmarkIfaceDD(b *testing.B) {
> var k K = &thinger{}
> t := &thing{}
> var n interface{} = 42
>
> for i := 0; i < b.N; i++ {
> k.d(t, n)
> }
> }
>
> where the argument is already an interface value and does not need to
> be boxed again. otherwise the optimiser can never know whether the
> argument escapes, unless it does very expensive whole program analysis.

Yes... I'm pretty sure you are right about that.

That was also my conclussion after I read this:

http://www.agardner.me/golang/garbage/collection/gc/escape/analysis/2015/10/18/go-escape-analysis.html

And the linked document:

https://docs.google.com/document/d/1CxgUBPlx9iJzkz9JWkb6tIpTe5q32QDmz8l0BouG0Cw/preview

/Peter

Peter Mogensen

unread,
May 27, 2016, 2:52:17 AM5/27/16
to golan...@googlegroups.com


On 2016-05-26 20:59, Peter Mogensen wrote:
>
>
> On 2016-05-26 20:18, Martin Schnabel wrote:
>> i think the situation you describe may be something like this:
>>
>> func BenchmarkIfaceDD(b *testing.B) {
>> var k K = &thinger{}
>> t := &thing{}
>> var n interface{} = 42
>>
>> for i := 0; i < b.N; i++ {
>> k.d(t, n)
>> }
>> }
>>
>> where the argument is already an interface value and does not need to
>> be boxed again. otherwise the optimiser can never know whether the
>> argument escapes, unless it does very expensive whole program analysis.
>
> Yes... I'm pretty sure you are right about that.
>

Ahh.. no... it's more complicated than that.
The only reason the above gets 0 allocations is that n is declared
outside the loop.

/Peter

Reply all
Reply to author
Forward
0 new messages