origin

These days, after reconstructing a certain code, I did a one-time test, and found a very strange call of runtime.newobject in the flame diagram, roughly accounting for 2%, but I searched the whole code and found no logic related to newobject. So forced, offering a assembly method, finally located the problem. This article shares this problem and the reasons behind it, using a minimum reproducible piece of code.

Show me the code

package main

import(_"unsafe"
)

type MyFunc func()

type myFuncImplStruct struct {
}

//go:noinline
func (m *myFuncImplStruct) myFunc() {
	return
}

//go:noinline
func (m myFuncImplStruct) myFunc2() {
	return
}

//go:noinline
func myFunc() {
	return
}

type myFuncContainer struct {
	f MyFunc
}

//go:noinline
func newFuncContainer(f MyFunc) *myFuncContainer {
	n := &myFuncContainer{}
	n.f = f
	return n
}

func main() {
	m := &myFuncImplStruct{}
	m2 := myFuncImplStruct{}
	c1 := newFuncContainer(myFunc)
	c2 := newFuncContainer(m.myFunc)
	c3 := newFuncContainer(m2.myFunc2)

	_, _, _ = c1, c2, c3
}
Copy the code

In this code, it may at first appear that there is no runtime allocation in main (regardless of the allocation caused by newFuncContainer), but there are actually two runtime allocations in main. What’s going on here?

Can the function escape to the heap?

-gcflags=”-m”

./main.go:13:7: m does not escape
./main.go:32:23: leaking param: f
./main.go:33:7: &myFuncContainer literal escapes to heap
./main.go:39:7: &myFuncImplStruct literal escapes to heap
./main.go:42:26: m.myFunc escapes to heap
./main.go:43:27: m2.myFunc2 escapes to heap
<autogenerated>:1: .this does not escape
Copy the code

That m. myfunc and m. MyFunc2 in lines 42 and 43 “escaped to the heap”? A function can escape to the heap.

The compaction hammer

It looks like this is the case, but we need to prove it, so we’ll use the assembly method (-gcflags=” -s “) to see what the generated assembly code looks like:

"".main STEXT size=160 args=0x0 locals=0x18...0x0031 00049 (main.go:42)	PCDATA	$0, $1
	0x0031 00049 (main.go:42)	LEAQ	type.noalg.struct { F uintptr; R *"".myFuncImplStruct }(SB), AX
	0x0038 00056 (main.go:42)	PCDATA	$0, $0
	0x0038 00056 (main.go:42)	MOVQ	AX, (SP)
	0x003c 00060 (main.go:42)	CALL	runtime.newobject(SB)
	0x0041 00065 (main.go:42)	PCDATA	$0, $1
	0x0041 00065 (main.go:42)	MOVQ	8(SP), AX
	0x0046 00070 (main.go:42)	LEAQ	"".(*myFuncImplStruct).myFunc-fm(SB), CX
	0x004d 00077 (main.go:42)	MOVQ	CX, (AX)
	0x0050 00080 (main.go:42)	PCDATA	$0, $2
	0x0050 00080 (main.go:42)	LEAQ	runtime.zerobase(SB), CX
	0x0057 00087 (main.go:42)	PCDATA	$0, $1
	0x0057 00087 (main.go:42)	MOVQ	CX, 8(AX)
	0x005b 00091 (main.go:42)	PCDATA	$0, $0
	0x005b 00091 (main.go:42)	MOVQ	AX, (SP)
	0x005f 00095 (main.go:42)	CALL	"".newFuncContainer(SB)
	0x0064 00100 (main.go:43)	PCDATA	$0, $1
	0x0064 00100 (main.go:43)	LEAQ	type.noalg.struct { F uintptr; R "".myFuncImplStruct }(SB), AX
	0x006b 00107 (main.go:43)	PCDATA	$0, $0
	0x006b 00107 (main.go:43)	MOVQ	AX, (SP)
	0x006f 00111 (main.go:43)	CALL	runtime.newobject(SB)
	0x0074 00116 (main.go:43)	PCDATA	$0, $1
	0x0074 00116 (main.go:43)	MOVQ	8(SP), AX
	0x0079 00121 (main.go:43)	LEAQ	"".myFuncImplStruct.myFunc2-fm(SB), CX
	0x0080 00128 (main.go:43)	MOVQ	CX, (AX)
	0x0083 00131 (main.go:43)	PCDATA	$0, $0
	0x0083 00131 (main.go:43)	MOVQ	AX, (SP)
	0x0087 00135 (main.go:43)	CALL	""NewFuncContainer (SB)...Copy the code

That’s a real blow. It’s really here, but why? Why does assigning a function to a variable cause a memory allocation? Isn’t the function name a pointer to the code address of the function?

Golang function call mechanism

In Golang, function calls are not as simple as C, and there are certain categories:

Function call classification

In Go, there are four types of functions:

  1. Top-level functions (ordinary functions)
  2. A function with a value receiver
  3. A function with a pointer receiver
  4. Function literal

There are five types of function calls:

  1. Call the top-level function directly
  2. Call the function with the value receiver directly
  3. Call the function that has a pointer receiver directly
  4. Call function value indirectly (func value)
  5. Call the interface function indirectly

The following example shows all possible function calls:

package main

func TopLevel(x int) {}

type Pointer struct{}

func (*Pointer) M(int) {}

type Value struct{}

func (Value) M(int) {}

type Interface interface{ M(int) }

var literal = func(x int) {}

func main() {
	// direct call of top-level func
	TopLevel(1)

	// direct call of method with value receiver (two spellings, but same)
	var v Value
	v.M(1)
	Value.M(v, 1)

	// direct call of method with pointer receiver (two spellings, but same)
	var p Pointer
	(&p).M(1)
	(*Pointer).M(&p, 1)

	// indirect call of func value (×4)
	f1 := TopLevel
	f1(1)
	f2 := Value.M
	f2(v, 1)
	f3 := (*Pointer).M
	f3(&p, 1)
	f4 := literal
	f4(1)

	// indirect call of method on interface (×3)
	var i Interface
	i = v
	i.M(1)
	i = &v
	i.M(1)
	i = &p
	i.M(1)
	Interface.M(i, 1)
	Interface.M(v, 1)
	Interface.M(&p, 1)}Copy the code

As shown in the above program, there are 10 possible call combinations:

  1. Call the top-level function/directly
  2. Call the value receiver function/directly
  3. Call the pointer receiver function/directly
  4. Indirect call function value (func value)/function value is the top-level function
  5. The indirect call function value/function value is the value receiver function
  6. The indirect call function value/function value is the pointer receiver function
  7. Indirectly call a function value/function value function literal
  8. Indirectly call the function /interface as the value, call the value receiver function
  9. The value receiver function is called indirectly
  10. Indirectly call the function /interface as a pointer, call the pointer receiver function

In the above list, the slash/left side is known at compile time and the right side is known at run time. The code generated at compile time does not know the information at run time, so additional Adapter functions need to be generated at run time to make indirect calls.

Function invocation implementation indirectly

Now, you can sort of guess why, as you can guess, in the program we started with, there was an indirect call, and the object that Go assigned to it had to do with the indirect call. Since there is nothing to say about direct calls, we will skip over them and only talk about indirect calls.

In Go, the implementation of indirect calls is shown below:

In effect, Go allocates an additional object, whose first field is a pointer to our actual function, and the second object is some data that is strongly related to the function (yes, that’s right, receiver). Thus, a function call actually generates code like the following:

MOV... , R0 MOV0(R0), R1
CALL R1  # called code can access “data” using R0

Copy the code

An exception is when a function has no associated data, such as a function literal that captures only external local variables, then the function has no associated data, so the memory layout is as follows:

In this scenario, Go optimizes the allocation of this variable in the read-only area and does not allocate it every time it is called, which generates the following code:

` `

MOV $MyFunc·f(SB), f1

DATA MyFunc·f(SB)/8, $MyFunc (SB) GLOBL MyFunc f. (SB),10, $8

Copy the code

So we don’t really have to worry too much about the performance loss in this scenario, which is zero.

For non-exceptional scenarios, an adapter function generates code like the following:

type funcValue struct {
	f uintptr // A pointer to a function
	r associatedType
}

// This is the actual function signaturefunc funcAdapter(...) (...). {r := (associatedType)(R0 + 8)
	return r.f(...)
}

f := &funcValue{funcAdapter, r}

Copy the code

When called, what is actually called is the adapter function, which then calls the real function.

Why do you do that?

Actually think is also very simple, for the value of the receiver and a pointer to the receiver function, called the first parameter to the self, so if I am now need to put a link on a specific value/pointer functions as a value assigned to a particular function variables, I also need to put together the corresponding value/pointer information with you, or when I really call, How do I know which value/pointer method to call? In other words, what value of self should be passed to the function?

All that said, why?

Going back to the problem we started with, we can see that the culprit for the two memory allocations has been found, and in assembly code we can see the culprit:

 0x0031 00049 (main.go:42)	PCDATA	$0, $1
0x0031 00049 (main.go:42)	LEAQ	type.noalg.struct { F uintptr; R *"".myFuncImplStruct }(SB), AX
0x0038 00056 (main.go:42)	PCDATA	$0, $0
0x0038 00056 (main.go:42)	MOVQ	AX, (SP)
0x003c 00060 (main.go:42)	CALL	runtime.newobject(SB)
0x0041 00065 (main.go:42)	PCDATA	$0, $1
0x0041 00065 (main.go:42)	MOVQ	8(SP), AX
0x0046 00070 (main.go:42)	LEAQ	"".(*myFuncImplStruct).myFunc-fm(SB), CX
0x004d 00077 (main.go:42)	MOVQ	CX, (AX)
0x0050 00080 (main.go:42)	PCDATA	$0, $2
0x0050 00080 (main.go:42)	LEAQ	runtime.zerobase(SB), CX
0x0057 00087 (main.go:42)	PCDATA	$0, $1
0x0057 00087 (main.go:42)	MOVQ	CX, 8(AX)
0x005b 00091 (main.go:42)	PCDATA	$0, $0
0x005b 00091 (main.go:42)	MOVQ	AX, (SP)
0x005f 00095 (main.go:42)	CALL	"".newFuncContainer(SB)
0x0064 00100 (main.go:43)	PCDATA	$0, $1
0x0064 00100 (main.go:43)	LEAQ	type.noalg.struct { F uintptr; R "".myFuncImplStruct }(SB), AX
0x006b 00107 (main.go:43)	PCDATA	$0, $0
0x006b 00107 (main.go:43)	MOVQ	AX, (SP)
0x006f 00111 (main.go:43)	CALL	runtime.newobject(SB)
0x0074 00116 (main.go:43)	PCDATA	$0, $1
0x0074 00116 (main.go:43)	MOVQ	8(SP), AX
0x0079 00121 (main.go:43)	LEAQ	"".myFuncImplStruct.myFunc2-fm(SB), CX
0x0080 00128 (main.go:43)	MOVQ	CX, (AX)
0x0083 00131 (main.go:43)	PCDATA	$0, $0
0x0083 00131 (main.go:43)	MOVQ	AX, (SP)
0x0087 00135 (main.go:43)	CALL	"".newFuncContainer(SB)

Copy the code

Struct {F uintptr; R *””. MyFuncImplStruct}(SB), AX (uintptr) The second is R *myFuncImplStruct; Struct {F uintptr; R “”. MyFuncImplStruct}(SB), AX, except that R is the value of myFuncImplStruct instead of a pointer, which is exactly what our code is.

conclusion

Ok, so basically the problem is clear, and optimization can be as simple as changing a function that doesn’t actually need a value receiver or pointer receiver to a top-level function, or avoiding indirect calls to a value receiver/pointer receiver function if possible.

As you can see, functions with receivers come at a cost, so don’t mess with them. The code design should be reasonable, otherwise it will introduce additional performance overhead.

The resources

Docs.google.com/document/d/…