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:
- Top-level functions (ordinary functions)
- A function with a value receiver
- A function with a pointer receiver
- Function literal
There are five types of function calls:
- Call the top-level function directly
- Call the function with the value receiver directly
- Call the function that has a pointer receiver directly
- Call function value indirectly (func value)
- 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:
- Call the top-level function/directly
- Call the value receiver function/directly
- Call the pointer receiver function/directly
- Indirect call function value (func value)/function value is the top-level function
- The indirect call function value/function value is the value receiver function
- The indirect call function value/function value is the pointer receiver function
- Indirectly call a function value/function value function literal
- Indirectly call the function /interface as the value, call the value receiver function
- The value receiver function is called indirectly
- 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/…