当前位置:网站首页>Go deep into the implementation principle of go language defer

Go deep into the implementation principle of go language defer

2022-06-24 16:15:00 luozhiyun

Please state the source of reprint ~, This article was published at luozhiyun The blog of : https://www.luozhiyun.com/archives/523

This article uses go Source code 1.15.7

Introduce

defer Enforcement rules

Multiple defer The execution order of is " Last in, first out LIFO "

package main

import (  
    "fmt"
)

func main() {  
    name := "Naveen"
    fmt.Printf("Original String: %s\n", string(name))
    fmt.Printf("Reversed String: ")
    for _, v := range []rune(name) {
        defer fmt.Printf("%c", v)
    }
} 

In the example above , Use for Loop the string Naveen After traversing defer, these defer The call is like being pressed on a stack , The last one to be pushed into the stack defer The call will be pulled out and executed first .

The output is as follows :

$ go run main.go 
Original String: Naveen
Reversed String: neevaN

defer When declared, the value of the determined parameter is calculated first

func a() {
    i := 0
    defer fmt.Println(i) // 0
    i++
    return
}

In this case , Variable i stay defer It's determined when it's called , Not in defer When it comes to execution , So the output of the above statement is 0.

defer You can modify the return value of a function with a known return value

As the official said :

For instance, if the deferred function is a function literal and the surrounding function has named result parameters that are in scope within the literal, the deferred function may access and modify the result parameters before they are returned.

What's said above is , If one is defer The function called is a function literal, That is, closures or anonymous functions , And call defer A function with a named return value (named result parameters) Function of , that defer You can directly access the return value and modify it .

Examples are as follows :

// f returns 42
func f() (result int) {
	defer func() {
		result *= 7
	}()
	return 6
}

But here's the thing , You can only modify the return value (named result parameters) function , Anonymous return value functions cannot be modified , as follows :

// f returns 100
func f() int {
	i := 100
	defer func() {
		i++
	}()
	return i
}

Because the anonymous return value function is in return Is declared when executed , So in defer Statement can only access the function with a named return value , You can't directly access anonymous return value functions .

defer The type of

Go stay 1.13 edition And 1.14 Version pair defer Two optimizations were carried out , bring defer The performance overhead of is greatly reduced in most scenarios .

Heap allocation

stay Go 1.13 All before defer It's all distributed on the heap , At compile time :

  1. stay defer Insert the position of the statement runtime.deferproc, When executed ,defer The call is saved as a runtime._defer Structure , Deposit in Goroutine Of _defer The front of the list ;
  2. Insert... Before the function returns runtime.deferreturn, When executed , From Goroutine Of _defer Take the first one from the list runtime._defer And in turn .

On the stack

Go 1.13 Version added deferprocStack Realized on the stack distribution defer, Compared to heap allocation , Stack allocation after function return _defer They are released , It saves the performance cost of memory allocation , Just maintain... Properly _defer A list of the links . According to the official documents , Doing so raises about 30% About performance .

Except for the distribution of positions , There is no essential difference between stack allocation and heap allocation .

It is worth noting that ,1.13 Not all of them in the version defer Can be distributed on the stack . In the loop defer, Whether it's displayed for loop , still goto The implicit loop formed , Can only use heap allocation , Even if you loop once, you can only use heap allocation :

func A1() {
	for i := 0; i < 1; i++ {
		defer println(i)
	}
}

$ GOOS=linux GOARCH=amd64 go tool compile -S main.go
        ...
        0x004e 00078 (main.go:5)        CALL    runtime.deferproc(SB)
        ...
        0x005a 00090 (main.go:5)        CALL    runtime.deferreturn(SB)
        0x005f 00095 (main.go:5)        MOVQ    32(SP), BP
        0x0064 00100 (main.go:5)        ADDQ    $40, SP
        0x0068 00104 (main.go:5)        RET

Open code

Go 1.14 The development code has been added to the version (open coded), The mechanism will defer Call the direct insert function to return before , Omit the runtime deferproc or deferprocStack operation ., This optimization can make defer The call overhead is from 1.13 Version of ~35ns Reduce to ~6ns about .

But you need to meet certain conditions to trigger :

  1. Compiler optimization is not disabled , That is, there is no setting -gcflags "-N";
  2. Within the function defer Not more than 8 individual , And return Statements and defer The product of the number of statements does not exceed 15;
  3. Functional defer Keywords cannot be executed in a loop ;

defer Structure

type _defer struct {
	siz     int32  		// Memory size of parameters and results 
	started bool
	heap    bool 		// Whether it's heap allocation 
    openDefer bool		//  Is it optimized by open coding 
	sp        uintptr   // Stack pointer 
	pc        uintptr   //  Caller's program counter 
	fn        *funcval 	//  Function passed in 
	_panic    *_panic   
	link      *_defer 	//defer Linked list 
	fd   unsafe.Pointer  
	varp uintptr        
	framepc uintptr
}

The above parameters that need to be noted are sizheapfnlinkopenDefer These parameters will be discussed in the following analysis .

analysis

In the analysis of this paper , Let's start with heap distribution , By the way defer Why is the execution rule of the law as mentioned at the beginning , And then we 'll talk about defer On the stack allocation and development coding related content .

The analysis starts with a function call as an entry , The students who don't understand the function call can have a look :《 Understand from the stack Go Language function call https://www.luozhiyun.com/archives/518 》.

Heap allocation

Call the return value of a well-known function

Let's start with the example mentioned above , Start with function calls and look at the heap allocation . It's important to note that I'm 1.15 edition Running the following example above does not directly allocate to the heap , I need to recompile it myself Go Source code let defer Force distribution on the heap :

file location :src/cmd/compile/internal/gc/ssa.go

func (s *state) stmt(n *Node) {
    ...
	case ODEFER: 
		if s.hasOpenDefers {
			s.openDeferRecord(n.Left)
		} else {
			d := callDefer
            //  It needs to be annotated here 
			// if n.Esc == EscNever {
			// 	d = callDeferStack
			// }
			s.call(n.Left, d)
		}
    ...
}

If you don't know how to compile , Take a look at this article :《 How to compile and debug Go runtime Source code https://www.luozhiyun.com/archives/506

func main() {
	f()
}

func f() (result int) {
	defer func() {
		result *= 7
	}() 
	return 6
}

Use the command to print the assembly :

$ GOOS=linux GOARCH=amd64 go tool compile -S -N -l main.go

Let's look at it first main function , There's nothing to say , Very simple call f function :

"".main STEXT size=54 args=0x0 locals=0x10
        0x0000 00000 (main.go:3)        TEXT    "".main(SB), ABIInternal, $16-0
        ...
        0x0020 00032 (main.go:4)        CALL    "".f(SB)
        ...

Let's take a look at it in sections f Function calls :

"".f STEXT size=126 args=0x8 locals=0x20
        0x0000 00000 (main.go:7)        TEXT    "".f(SB), ABIInternal, $32-8 
        ...
        0x001d 00029 (main.go:7)        MOVQ    $0, "".result+40(SP)        ;;  Constant 0  write in 40(SP)  
        0x0026 00038 (main.go:8)        MOVL    $8, (SP)                    ;;  Constant 8  Put it on the top of the stack 
        0x002d 00045 (main.go:8)        LEAQ    "".f.func1·f(SB), AX        ;;  Will function f.func1·f Address write AX
        0x0034 00052 (main.go:8)        MOVQ    AX, 8(SP)                   ;;  Will function f.func1·f Address write 8(SP)
        0x0039 00057 (main.go:8)        LEAQ    "".result+40(SP), AX        ;;  take 40(SP) The address value is written to AX
        0x003e 00062 (main.go:8)        MOVQ    AX, 16(SP)                  ;;  take AX  The saved address is written to 16(SP)
        0x0043 00067 (main.go:8)        PCDATA  $1, $0
        0x0043 00067 (main.go:8)        CALL    runtime.deferproc(SB)       ;;  call  runtime.deferproc  function 

because defer The heap allocation calls runtime.deferproc function , So what's shown in this compilation is runtime.deferproc A piece of assembly before a function call , If seen 《 Understand from the stack Go Language function call https://www.luozhiyun.com/archives/518 》, So the above paragraph is very simple to understand .

because runtime.deferproc The arguments to a function are just two arguments , as follows :

func deferproc(siz int32, fn *funcval)

During a function call , The parameter passing is Press the stack from right to left in the parameter list , So at the top of the stack are constants 8, stay 8(SP) The position pressed in is the second parameter f.func1·f Function address .

There may be a question here , In the press in constant 8 When the size is int32 Occupy 4 Byte size , Why doesn't the second parameter come from 4(SP) Start , But from 8(SP) Start , This is due to the need for memory alignment .

In addition to parameters , The other thing to note is that 16(SP) What's pressed into the position is 40(SP) Address value of . So the whole stack structure before calling should be as follows :

defer_call

Let's take a look runtime.deferproc:

file location :src/runtime/panic.go

func deferproc(siz int32, fn *funcval) {  
	if getg().m.curg != getg() { 
		throw("defer on system stack")
	}
	//  obtain sp The pointer 
	sp := getcallersp()
	//  obtain fn Function with a pointer as an argument 
	argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
	callerpc := getcallerpc()
	//  Get a new one defer
	d := newdefer(siz)
	if d._panic != nil {
		throw("deferproc: d.panic != nil after newdefer")
	}
    //  take  defer  Add to the list 
	d.link = gp._defer
	gp._defer = d
	d.fn = fn
	d.pc = callerpc
	d.sp = sp
    //  Copy the parameters 
	switch siz {
	case 0: 
        // If defered If the parameter of a function has only the size of a pointer, it copies the parameter directly through assignment 
	case sys.PtrSize:
		//  take  argp  The corresponding value   Write to  deferArgs  In the returned address 
		*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
	default:
        //  If the parameter size is not the pointer size , So make a data copy 
		memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
	}
 
	return0() 
}

Calling deferproc Function , We know , Parameters siz The value passed in is the value at the top of the stack, which means that the parameter size is 8 , Parameters fn Incoming 8(SP) The corresponding address .

argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
...
*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))

So the combination of the above two sentences is actually a combination of the above two sentences 16(SP) The address values saved in are saved in defer Next next next 8bytes Memory block as defer Parameters of . A simple sketch should be as follows ,defer Next to it argp What's actually in it is 16(SP) The address value saved in :

defer_call2

It should be noted that , This will be copied through the copy operation argp value , So in defer When called , The parameters have been determined , Instead of waiting for the execution to decide , But here's a copy of the address value .

And we know , When allocating on the heap ,defer It will be stored in the current Goroutine in , If there is 3 individual defer Called separately , Then the last call will be at the front of the linked list :

deferLink

about newdefer This function is generally from P In the local cache pool of , If you can't get it, you can get it from sched Get half of it in the global cache pool of defer Fill in P Local resource pool for , If there is still no cache available , Allocate new... Directly from the heap defer and args . The idea of memory allocation and memory allocator allocation is similar , No more analysis , If you are interested, you can have a look at it yourself .

Now let's go back to f In the assembly of functions :

"".f STEXT size=126 args=0x8 locals=0x20 
        ...
        0x004e 00078 (main.go:11)       MOVQ    $6, "".result+40(SP)        ;;  Constant 6 write in 40(SP), As return value 
        0x0057 00087 (main.go:11)       XCHGL   AX, AX
        0x0058 00088 (main.go:11)       CALL    runtime.deferreturn(SB)     ;;  call  runtime.deferreturn  function 
        0x005d 00093 (main.go:11)       MOVQ    24(SP), BP
        0x0062 00098 (main.go:11)       ADDQ    $32, SP
        0x0066 00102 (main.go:11)       RET

It's very simple here , Just put the constant 6 Write to 40(SP) As the return value , And then call runtime.deferreturn perform defer.

Let's take a look runtime.deferreturn:

file location :src/runtime/panic.go

func deferreturn(arg0 uintptr) {
	gp := getg()
	d := gp._defer
	if d == nil {
		return
	}
	//  determine  defer  Is the caller of the current  deferreturn  The caller of 
	sp := getcallersp()
	if d.sp != sp {
		return
	}
	 
	switch d.siz {
	case 0:
		// Do nothing.
	case sys.PtrSize:
		//  take  defer  Copy the saved parameters 
		// arg0  It's actually  caller SP  Top of stack address value , So here you're actually copying the parameters to  caller SP  Top of stack address value 
		*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
	default:
        //  If the parameter size is not  sys.PtrSize, So make a data copy 
		memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
	}
	fn := d.fn
	d.fn = nil
	gp._defer = d.link
	// take  defer  The object is put into  defer  In the pool , It can be reused later 
	freedefer(d)
	 
	_ = fn.fn
	//  Pass in the functions and parameters to be executed 
	jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}

The first thing to notice is , The parameter passed in here arg0 It's actually caller The value at the top of the stack for the caller , So the following assignment is actually to defer Copy the parameters of to caller The top of the caller stack :

*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))

*(*uintptr)(deferArgs(d)) What's kept here is actually caller The caller 16(SP) Saved address values . that caller The stack frame of the caller is shown in the following figure :

callerstackframe

Let's go down to runtime.jmpdefer Take a look at how to achieve :

Location :src/runtime/asm_amd64.s

TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16
	MOVQ	fv+0(FP), DX	// fn  Function address 
	MOVQ	argp+8(FP), BX	// caller sp  The caller  SP
	LEAQ	-8(BX), SP	//  caller  The caller after  SP
	MOVQ	-8(SP), BP	//  caller  The caller after  BP
	SUBQ	$5, (SP)	//   obtain  runtime.deferreturn  The address value is written to the top of the stack 
	MOVQ	0(DX), BX   // BX = DX
	JMP	BX	//  Execution by  defer  Function of 

This compilation is very interesting ,jmpdefer Function because it is runtime.deferreturn Called , So now the call stack frame is :

callingchain

The incoming to jmpdefer The argument to the function is 0(FP) Express fn Function address , as well as 8(FP) It means f Function call stack SP.

callingchain2

So the following sentence stands for runtime.deferreturn Call stack's return address Write to SP:

LEAQ	-8(BX), SP

that -8(SP) It stands for runtime.deferreturn Call stack's Base Pointer

MOVQ	-8(SP), BP

Now let's focus on explaining why SP The value the pointer points to minus 5 Can be obtained runtime.deferreturn Address value of :

SUBQ	$5, (SP)

We go back to f In the assembly of function calls :

(dlv) disass
TEXT main.f(SB) /data/gotest/main.go
		...
        main.go:11      0x45def8        e8a3e2fcff              call $runtime.deferreturn
        main.go:11      0x45defd        488b6c2418              mov rbp, qword ptr [rsp+0x18]
        ...

Due to the end of the call runtime.deferreturn Function, you need to continue to return to 0x45defd Continue at address value , So in calling runtime.deferreturn Function in the corresponding stack frame return address In fact, that is 0x45defd.

And in the jmpdefer Function ,(SP) The corresponding value is runtime.deferreturn Call stack's return address, So will 0x45defd subtract 5 Just in time to get 0x45def8 , And this value is runtime.deferreturn The address value of the function .

So at the end, jump to f.func1 Function execution time , The call stack is as follows :

callingchain3

The call stack (SP) In fact, the location of the store points to deferreturn Pointer to function , So in f.func1 After the function is called, it will return to deferreturn function , until _defer Until there is no data in the chain :

func deferreturn(arg0 uintptr) {
	gp := getg()
	d := gp._defer
	if d == nil {
		return
	}
	...
}

Let's take a brief look at f.func1 Function call :

"".f.func1 STEXT nosplit size=25 args=0x8 locals=0x0
        0x0000 00000 (main.go:8)        TEXT    "".f.func1(SB), NOSPLIT|ABIInternal, $0-8
        0x0000 00000 (main.go:8)        FUNCDATA        $0, gclocals·1a65e721a2ccc325b382662e7ffee780(SB)
        0x0000 00000 (main.go:8)        FUNCDATA        $1, gclocals·69c1753bd5f81501d95132d08af04464(SB)
        0x0000 00000 (main.go:9)        MOVQ    "".&result+8(SP), AX        ;;  Will point to 6 Write the address value of  AX
        0x0005 00005 (main.go:9)        MOVQ    (AX), AX                    ;;  take  6  Write to  AX
        0x0008 00008 (main.go:9)        LEAQ    (AX)(AX*2), CX              ;; CX = 6*2 +6 =18
        0x000c 00012 (main.go:9)        LEAQ    (AX)(CX*2), AX              ;; AX = 18*2 + 6 =42
        0x0010 00016 (main.go:9)        MOVQ    "".&result+8(SP), CX        ;;  Will point to 6 Write the address value of  CX
        0x0015 00021 (main.go:9)        MOVQ    AX, (CX)                    ;;  take CX The value pointed to by the address value is changed to 42
        0x0018 00024 (main.go:10)       RET

The call here is very simple , Get 8(SP) The address value points to the data and then does the operation , Then write the result to the stack , return .

So here we basically put defer The whole process of the function call is shown to you through heap allocation . From the above analysis, we have basically answered defer You can modify how the return value of a function with a known return value is achieved , The answer is in fact defer Passed during the call defer A parameter is a pointer to a return value , So in the end defer The return value will be modified during execution .

Anonymous function return value call

So how is the anonymous return value function passed ? For example, the following :

// f returns 100
func f() int {
	i := 100
	defer func() {
		i++
	}()
	return i
}

Now print the assembly :

"".f STEXT size=139 args=0x8 locals=0x28
        0x0000 00000 (main.go:7)        TEXT    "".f(SB), ABIInternal, $40-8
        ...
        0x001d 00029 (main.go:7)        MOVQ    $0, "".~r0+48(SP)       ;; Initialization return value 
        0x0026 00038 (main.go:8)        MOVQ    $100, "".i+24(SP)       ;; Initialize parameters i
        0x002f 00047 (main.go:9)        MOVL    $8, (SP)
        0x0036 00054 (main.go:9)        LEAQ    "".f.func1·f(SB), AX
        0x003d 00061 (main.go:9)        MOVQ    AX, 8(SP)               ;;  take f.func1·f The address value is written to 8(SP)
        0x0042 00066 (main.go:9)        LEAQ    "".i+24(SP), AX
        0x0047 00071 (main.go:9)        MOVQ    AX, 16(SP)              ;;  take  24(SP)  The address value is written to  16(SP) 
        0x004c 00076 (main.go:9)        PCDATA  $1, $0
        0x004c 00076 (main.go:9)        CALL    runtime.deferproc(SB)
        0x0051 00081 (main.go:9)        TESTL   AX, AX
        0x0053 00083 (main.go:9)        JNE     113
        0x0055 00085 (main.go:9)        JMP     87
        0x0057 00087 (main.go:12)       MOVQ    "".i+24(SP), AX         ;;  take 24(SP) Value 100 Write to AX
        0x005c 00092 (main.go:12)       MOVQ    AX, "".~r0+48(SP)       ;;  Will value 100 Write to 48(SP)
        0x0061 00097 (main.go:12)       XCHGL   AX, AX
        0x0062 00098 (main.go:12)       CALL    runtime.deferreturn(SB)
        0x0067 00103 (main.go:12)       MOVQ    32(SP), BP
        0x006c 00108 (main.go:12)       ADDQ    $40, SP
        0x0070 00112 (main.go:12)       RET

In the above output, we can see that in the call of anonymous return value function, the constant will be used first 100 Write to 24(SP) in , And then 24(SP) Write the address value of to 16(SP) , And then when you write the return value, it's through MOVQ Instructions will be 24(SP) The value of is written to 48(SP) in , That is to say, it's all value copying , There is no copy pointer , So there is no change to the return value .

Summary

Let's compare the two through a diagram after calling runtime.deferreturn Stack frame situation :

stackframecompare

It's obvious that the famous return value function will be in 16(SP) The address where the return value is stored , And the anonymous return value function will be in 16(SP) It's a place to keep 24(SP) The address of .

Through the above series of analysis, I also answered a few questions :

  1. defer How to pass parameters ?

When we analyze it above, we will find that , In execution deferproc Function will first copy the parameter value to defer The location next to the memory address value is used as a parameter , If the pointer is passed, the pointer will be copied directly , Value passing copies the value directly to defer Location of parameters .

deferargs

And then in the deferreturn Function will copy the parameter values to the stack , And then call jmpdefer To perform .

func deferreturn(arg0 uintptr) {

...

switch d.siz {

case 0:

// Do nothing.

case sys.PtrSize:

// take defer Copy the saved parameters

// arg0 It's actually caller SP Top of stack address value , So here you're actually copying the parameters to caller SP Top of stack address value

(uintptr)(unsafe.Pointer(&arg0)) = (uintptr)(deferArgs(d))

default:

       //  If the parameter size is not  sys.PtrSize, So make a data copy 

memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))

}

...

}

  1. Multiple defer How statements are executed ?

Calling deferproc Function registration defer The new element will be inserted in the header when it is used , When executing, it also obtains the chain header and executes in turn .

deferLink
  1. defer、return、 What is the execution order of the return value ?

For this question , Let's take the assembly of the output in the above example and study it :

"".f STEXT size=126 args=0x8 locals=0x20

       ...
       0x004e 00078 (main.go:11)       MOVQ    $6, "".result+40(SP)        ;;  Constant 6 write in 40(SP), As return value 
       0x0057 00087 (main.go:11)       XCHGL   AX, AX
       0x0058 00088 (main.go:11)       CALL    runtime.deferreturn(SB)     ;;  call  runtime.deferreturn  function 
       0x005d 00093 (main.go:11)       MOVQ    24(SP), BP
       0x0062 00098 (main.go:11)       ADDQ    $32, SP
       0x0066 00102 (main.go:11)       RET

You can see from this compilation that , about

  1. The first is to set the return value to constant first 6;
  2. And then call runtime.deferreturn perform defer Linked list ;
  3. perform RET The command jumps to caller function ;

On the stack

I talked about it at the beginning , stay Go Of 1.13 After the release, we added defer On the stack of , So one difference from heap allocation is that it's created on the stack defer It's through deferprocStack To create .

Go At compile time in SSA The stage will be judged , If it's stack allocation , Then you need to use the compiler directly on the function call frame to initialize _defer Record , And pass it as a parameter to deferprocStack. There is no difference between other execution processes and heap allocation .

about deferprocStack Function let's take a brief look at :

file location :src/cmd/compile/internal/gc/ssa.go

func deferprocStack(d *_defer) {
	gp := getg()
	if gp.m.curg != gp { 
		throw("defer on system stack")
	} 
	d.started = false
	d.heap = false  //  Distributed on the stack  _defer
	d.openDefer = false
	d.sp = getcallersp()
	d.pc = getcallerpc()
	d.framepc = 0
	d.varp = 0 
	*(*uintptr)(unsafe.Pointer(&d._panic)) = 0
	*(*uintptr)(unsafe.Pointer(&d.fd)) = 0
	//  Will be multiple  _defer  Records are concatenated through linked lists 
	*(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))
	*(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))

	return0() 
}

The main function is to give _defer Structure assignment , And back to .

Open code

Go Language in 1.14 Through code inline optimization , Make the end of the function directly to defer Function to call , There's almost no extra cost to do it . stay SSA The construction phase of buildssa It will check whether the conditions are met according to , Open coding will be inserted only if the conditions are met , because SSA The code in the build phase of is not easy to understand , So here's just the basic principle , No code analysis .

We can assemble and print the example of allocation on the heap :

$ GOOS=linux GOARCH=amd64 go tool compile -S  main.go
"".f STEXT size=155 args=0x8 locals=0x30
        0x0000 00000 (main.go:7)        TEXT    "".f(SB), ABIInternal, $48-8
        ...
        0x002e 00046 (main.go:7)        MOVQ    $0, "".~r0+56(SP)
        0x0037 00055 (main.go:8)        MOVQ    $100, "".i+16(SP)
        0x0040 00064 (main.go:9)        LEAQ    "".f.func1·f(SB), AX
        0x0047 00071 (main.go:9)        MOVQ    AX, ""..autotmp_4+32(SP)
        0x004c 00076 (main.go:9)        LEAQ    "".i+16(SP), AX
        0x0051 00081 (main.go:9)        MOVQ    AX, ""..autotmp_5+24(SP)
        0x0056 00086 (main.go:9)        MOVB    $1, ""..autotmp_3+15(SP)
        0x005b 00091 (main.go:12)       MOVQ    "".i+16(SP), AX
        0x0060 00096 (main.go:12)       MOVQ    AX, "".~r0+56(SP)
        0x0065 00101 (main.go:12)       MOVB    $0, ""..autotmp_3+15(SP)
        0x006a 00106 (main.go:12)       MOVQ    ""..autotmp_5+24(SP), AX
        0x006f 00111 (main.go:12)       MOVQ    AX, (SP)
        0x0073 00115 (main.go:12)       PCDATA  $1, $1
        0x0073 00115 (main.go:12)       CALL    "".f.func1(SB)	;;  Call directly  defer  function 
        0x0078 00120 (main.go:12)       MOVQ    40(SP), BP
        0x007d 00125 (main.go:12)       ADDQ    $48, SP
        0x0081 00129 (main.go:12)       RET

We can see that in the assembly output above, we directly put defer The function is inserted at the end of the function and called .

The example above is easy to optimize , But if one defer In a conditional statement , This condition cannot be determined until runtime , So how to optimize it ?

It's also used in open coding defer bit Delay bit to determine whether the conditional branch should be executed . This The delay bit length is a 8 Bit binary code , So in this optimization, you can only use at most 8 individual defer, Including in the conditional judgment defer. Whether each bit is set to 1, To determine whether the delay statement is set at run time , If you set , The call . Otherwise... Is not called .

For example, an example is explained in the following article :

https://go.googlesource.com/proposal/+/refs/heads/master/design/34481-opencoded-defers.md

defer f1(a)
if cond {
 defer f2(b)
}
body...

In the phase of creating deferred calls , First of all, the conditional ones are recorded by the specific position of the delay bit defer Be triggered .

deferBits := 0           //  Initial value  00000000
deferBits |= 1 << 0     //  The first one  defer, Set to  00000001
_f1 = f1
_a1 = a1
if cond {
	//  If the second one  defer  Set up , Is set to  00000011, Otherwise, it will still be  00000001
	deferBits |= 1 << 1
	_f2 = f2
	_a2 = a2
}

Before the function returns and exits , exit The function creates a check code for the delay bits in reverse order :

exit:
//  Judge  deferBits & 00000010 == 00000010 Is it true 
if deferBits & 1<<1 != 0 {
 deferBits &^= 1<<1
 tmpF2(tmpB)
}
//  Judge  deferBits & 00000001  == 00000001  Is it true 
if deferBits & 1<<0 != 0 {
 deferBits &^= 1<<0
 tmpF1(tmpA)
}

Before the function exits, it will judge whether the delay bit is equal to the value of the corresponding position 1, If 1, So it means that the defer function .

summary

This article mainly explains defer Implementation rules of , And right defer Types are introduced . Mainly through the heap allocation to explain defer How function calls are done , Such as : Function calls are used to understand " defer Parameter transfer of "、" Multiple defer How statements are executed "、" as well as defer、return、 What is the execution order of the return value " And so on . Through this analysis, I hope you can understand defer Can have a deeper understanding of .

Reference

Delay statement https://golang.design/under-the-hood/zh-cn/part1basic/ch03lang/defer/

defer https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-defer/#53-defer

Defer statements https://golang.org/ref/spec#Defer_statements

Proposal: Low-cost defers through inline code, and extra funcdata to manage the panic case https://go.googlesource.com/proposal/+/refs/heads/master/design/34481-opencoded-defers.md

Defer in Practice https://exlskills.com/learn-en/courses/aap-learn-go-golang--learn_golang_asap/aap-learn--asapgo/beyond-the-basics-YvUaZMowIAIo/defer-aqcApCogZxuV

Completely transformed defer https://mp.weixin.qq.com/s/gaC2gmFhJezH-9-uxpz07w

Go defer In depth analysis (3)—— Source code analysis , Deep principle analysis https://zhuanlan.zhihu.com/p/351177696

Inlined defers in Go https://rakyll.org/inlined-defers/

Go How a program is compiled into object machine code https://segmentfault.com/a/1190000016523685

luozhiyun cool
原网站

版权声明
本文为[luozhiyun]所创,转载请带上原文链接,感谢
https://yzsam.com/2021/05/20210501132816399T.html