Go语言:Go 语言之 defer 的前世今生( 四 )
deferreturn 被编译器插入到函数末尾 , 当跳转到它时 , 会将需要被 defer 的入口地址取出 , 然后跳转并执行:
1// src/runtime/panic.go23//go:nosplit4func deferreturn(arg0 uintptr) {5 gp := getg6 d := gp._defer7 if d == nil {8 return9 }10 // 确定 defer 的调用方是不是当前 deferreturn 的调用方11 sp := getcallersp12 if d.sp != sp {13 return14 }15 ...1617 // 将参数复制出 _defer 记录外18 switch d.siz {19 case 0: // 什么也不做20 case sys.PtrSize:21 *(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))22 default:23 memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))24 }25 // 获得被延迟的调用 fn 的入口地址 , 并随后立即将 _defer 释放掉26 fn := d.fn27 d.fn = nil28 gp._defer = d.link29 freedefer(d)3031 // 调用 , 并跳转到下一个 defer32 jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))33}
在这个函数中 , 会在需要时对 defer 的参数再次进行拷贝 , 多个 defer 函数以 jmpdefer 尾调用形式被实现 。 在跳转到 fn 之前 , _defer 实例被释放归还 , jmpdefer 真正需要的仅仅只是函数的入口地址和参数 , 以及它的调用方 deferreturn 的 SP:
1// src/runtime/asm_amd64.s23// func jmpdefer(fv *funcval, argp uintptr)4TEXT runtime·jmpdefer(SB), NOSPLIT, $0-165 MOVQ fv+0(FP), DX // DX = fn6 MOVQ argp+8(FP), BX // 调用方 SP7 LEAQ -8(BX), SP // CALL 后的调用方 SP8 MOVQ -8(SP), BP // 恢复 BP , 好像 deferreturn 返回9 SUBQ $5, (SP) // 再次返回到 CALL10 MOVQ 0(DX), BX // BX = DX11 JMP BX // 最后才运行被 defer 的函数
这个 jmpdefer 巧妙的地方在于 , 它通过调用方 SP 来推算了 deferreturn的入口地址 , 从而在完成某个 defer 调用后 , 由于被 defer 的函数返回时会出栈 , 会再次回到 deferreturn 的初始位置 , 进而继续反复调用 , 从而模拟 deferreturn 不断地对自己进行尾递归的假象 。
释放操作非常普通 , 只是简单地将其归还到 P 的 deferpool 中 ,并在本地池已满时将其归还到全局资源池:
1// src/runtime/panic.go23//go:nosplit4func freedefer(d *_defer) {5 ...6 sc := deferclass(uintptr(d.siz))7 if sc >= uintptr(len(p{}.deferpool)) {8 return9 }10 pp := getg.m.p.ptr11 // 如果 P 本地池已满 , 则将一半资源放入全局池 , 同样也是出于性能考虑12 // 操作会切换到系统栈上执行 。 13 if len(pp.deferpool[sc]) == cap(pp.deferpool[sc]) {14 systemstack(func {15 var first, last *_defer16 for len(pp.deferpool[sc]) > cap(pp.deferpool[sc])/2 {17 n := len(pp.deferpool[sc])18 d := pp.deferpool[sc][n-1]19 pp.deferpool[sc][n-1] = nil20 pp.deferpool[sc] = pp.deferpool[sc][:n-1]21 if first == nil {22 first = d23 } else {24 last.link = d25 }26 last = d27 }28 lock(&sched.deferlock)29 last.link = sched.deferpool[sc]30 sched.deferpool[sc] = first31 unlock(&sched.deferlock)32 })33 }3435 // 恢复 _defer 的零值 , 即 *d = _defer{}36 d.siz = 037 ...38 d.sp = 039 d.pc = 040 d.framepc = 041 ...42 d.link = nil4344 // 放入 P 本地资源池45 pp.deferpool[sc] = append(pp.deferpool[sc], d)46}
本文插图
在栈上创建 defer defer 还可以直接在栈上进行分配 , 也就是第二种记录 defer 的形式 deferprocStack 。 在栈上分配 defer 的好处在于函数返回后 _defer 便已得到释放 , 不再需要考虑内存分配时产生的性能开销 , 只需要适当地维护 _defer 的链表即可 。
- 【手机中国】华为又一项黑科技即将来临:可即时翻译任何动物语言
- [动物]兽语十级:华为愚人节成功研制出“动物语言翻译机”
- 上线■商务印书馆语言资源知识服务平台上线
- [C语言]Cu002FC++中的内存四区
- 汇编语言■C|编程的一些前置知识及底层(计算机组成与汇编)了解
- 【联合国】联合国最终确定了“6种”世界通用语言,日语的申请遭到拒绝!
- 净利润■主力洗盘之前,“盘口语言”都会出现这种征兆,看懂扭亏为盈!
- 「C语言」学了这么久的C语言,你真的懂scanf函数么?
- 『华为Mate30』华为推出国际版语音助手Celia:P40已预装、能听说三门语言
- 「编程语言」2020年,5 种 将死的编程语言