Skip to content

Commit 5314c42

Browse files
committed
1.14 defer done
1 parent cda94e9 commit 5314c42

File tree

1 file changed

+150
-2
lines changed

1 file changed

+150
-2
lines changed

1.14/defer.md

Lines changed: 150 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,41 @@
44

55
在 Go 1.14 中,增加了一种新的 defer 实现:open coded defer。当函数内 defer 不超过 8 个时,则会使用这种实现。
66

7+
形如下面这样的代码:
8+
9+
```go
10+
defer f1(a)
11+
if cond {
12+
defer f2(b)
13+
}
14+
body...
15+
```
16+
17+
会被翻译为:
18+
19+
```go
20+
deferBits |= 1<<0 // 因为函数主 block 有 defer 语句,所以这里要设置第 0 位为 1
21+
tmpF1 = f1 // 将函数地址搬运到预留的栈上空间
22+
tmpA = a // 将函数参数复制到预留的栈上空间
23+
if cond {
24+
deferBits |= 1<<1 // 因为进入了 if block,所以这里要设置第 1 位为 1
25+
tmpF2 = f2 // 将函数地址搬运到预留的栈上空间
26+
tmpB = b // 将函数参数复制到预留的栈上空间
27+
}
28+
body...
29+
exit: // 退出函数 body
30+
if deferBits & 1<<1 != 0 { // 检查第 1 个 bit,如果是 1,执行保存好的函数
31+
deferBits &^= 1<<1
32+
tmpF2(tmpB)
33+
}
34+
if deferBits & 1<<0 != 0 { // 检查第 0 个 bit,如果是 1,执行保存好的函数
35+
deferBits &^= 1<<0
36+
tmpF1(tmpA)
37+
}
38+
```
39+
40+
这样看我们不一定知道他具体是怎么实现的,可以写个 block 更多的例子来看看实际生成的代码是什么样的:
41+
742
```go
843
1 package main
944
2
@@ -38,7 +73,7 @@
3873
31 }
3974
```
4075

41-
可以分析上面的代码生成的汇编来理解这个新版的 open coded defer 到底是怎么实现的:
76+
下面是 go tool compile -S 生成的汇编代码,简单划分一下 block 来理解一下:
4277

4378
```
4479
"".main STEXT size=1125 args=0x0 locals=0x160
@@ -278,6 +313,8 @@ DEFER 逻辑执行部分
278313
0x0460 01120 (open-coded-defer.go:7) JMP 0
279314
```
280315

316+
整体流程还好,不过比起以前堆上分配一个 `_defer` 链表,函数执行完之后直接遍历链表还是复杂太多了。
317+
281318
超过 8 个 defer 时会退化回以前的 defer 链,也可以观察一下:
282319

283320
```go
@@ -299,12 +336,123 @@ func main() {
299336

300337
编译出的汇编就不贴了,和以前的没什么区别。
301338

302-
可以做个简单的总结了:
339+
结合代码分析过程和 proposal 中的描述,总结:
303340

304341
* open coded defer 在函数内部总 defer 数量少于 8 时才会使用,大于 8 时会退化回老的 defer 链,这个权衡是考虑到程序文件的体积(个人觉得应该也有栈膨胀的考虑在)
305342
* 使用 open coded defer 时,在进入函数内的每个 block 时,都会设置这个 block 相应的 bit(ORL 指令),在执行到相应的 defer 语句时,把 defer 语句的参数和调用函数地址推到栈的相应位置
306343
* 在栈上需要为这些 defer 调用的参数、函数地址,以及这个用来记录 defer bits 的 byte 预留空间,所以毫无疑问,函数的 framesize 会变大
307344

345+
## panic 处理部分
346+
347+
proposal 有两部分,一部分是讲 open coded defer 的实现,另一部分是说 panic 的处理,现在在函数中发生 panic 时,我们没有办法获得原来的 `_defer` 结构体了,所以必须在编译期,给每个函数准备一项 FUNCDATA,存储每一个 defer block 的地址。这样 runtime 就可以通过 FUNCDATA 和偏移找到需要执行的 defer block 在什么地方。
348+
349+
```
350+
0x004a 00074 (defer.go:8) FUNCDATA $2, gclocals·951c056c1b0a4d6097d387fbe928bbbf(SB)
351+
0x004a 00074 (defer.go:8) FUNCDATA $3, "".main.stkobj(SB)
352+
0x004a 00074 (defer.go:8) FUNCDATA $5, "".main.opendefer(SB)
353+
354+
....
355+
356+
"".main.opendefer SRODATA dupok size=29
357+
0x0000 30 c1 01 04 30 80 01 01 60 18 00 30 78 01 48 18 0...0...`..0x.H.
358+
0x0010 00 30 70 01 30 18 00 30 68 01 18 18 00 .0p.0..0h....
359+
```
360+
361+
这里的 main.opendefer 在编译为二进制之后,可能会被优化掉。不过我们理解基本的执行流程就可以了。
362+
363+
跟踪 runtime.gopanic 的流程,可以看到现在的 defer 上有 openDefer 的 bool 值:
364+
365+
```
366+
(dlv) p d
367+
*runtime._defer {
368+
siz: 48,
369+
started: true,
370+
heap: false,
371+
openDefer: false,
372+
sp: 824634391712,
373+
pc: 17590156,
374+
fn: *runtime.funcval {fn: 17565744},
375+
_panic: *runtime._panic nil,
376+
link: *runtime._defer {
377+
siz: 8,
378+
started: false,
379+
heap: true,
380+
openDefer: true,
381+
sp: 824634392456,
382+
pc: 17001293,
383+
fn: *runtime.funcval nil,
384+
_panic: *runtime._panic nil,
385+
link: *runtime._defer nil,
386+
fd: unsafe.Pointer(0x10fea48),
387+
varp: 824634392528,
388+
framepc: 17000984,},
389+
fd: unsafe.Pointer(0x0),
390+
varp: 0,
391+
framepc: 0,}
392+
(dlv) n
393+
```
394+
395+
执行具体的 defer 逻辑在 runtime.runOpenDeferFrame,也没啥神奇的:
396+
397+
```go
398+
func runOpenDeferFrame(gp *g, d *_defer) bool {
399+
done := true
400+
fd := d.fd
401+
402+
// Skip the maxargsize
403+
_, fd = readvarintUnsafe(fd)
404+
deferBitsOffset, fd := readvarintUnsafe(fd)
405+
nDefers, fd := readvarintUnsafe(fd)
406+
deferBits := *(*uint8)(unsafe.Pointer(d.varp - uintptr(deferBitsOffset)))
407+
408+
for i := int(nDefers) - 1; i >= 0; i-- {
409+
// read the funcdata info for this defer
410+
var argWidth, closureOffset, nArgs uint32
411+
argWidth, fd = readvarintUnsafe(fd)
412+
closureOffset, fd = readvarintUnsafe(fd)
413+
nArgs, fd = readvarintUnsafe(fd)
414+
if deferBits&(1<<i) == 0 {
415+
for j := uint32(0); j < nArgs; j++ {
416+
_, fd = readvarintUnsafe(fd)
417+
_, fd = readvarintUnsafe(fd)
418+
_, fd = readvarintUnsafe(fd)
419+
}
420+
continue
421+
}
422+
closure := *(**funcval)(unsafe.Pointer(d.varp - uintptr(closureOffset)))
423+
d.fn = closure
424+
deferArgs := deferArgs(d)
425+
// If there is an interface receiver or method receiver, it is
426+
// described/included as the first arg.
427+
for j := uint32(0); j < nArgs; j++ {
428+
var argOffset, argLen, argCallOffset uint32
429+
argOffset, fd = readvarintUnsafe(fd)
430+
argLen, fd = readvarintUnsafe(fd)
431+
argCallOffset, fd = readvarintUnsafe(fd)
432+
memmove(unsafe.Pointer(uintptr(deferArgs)+uintptr(argCallOffset)),
433+
unsafe.Pointer(d.varp-uintptr(argOffset)),
434+
uintptr(argLen))
435+
}
436+
deferBits = deferBits &^ (1 << i)
437+
*(*uint8)(unsafe.Pointer(d.varp - uintptr(deferBitsOffset))) = deferBits
438+
p := d._panic
439+
reflectcallSave(p, unsafe.Pointer(closure), deferArgs, argWidth)
440+
if p != nil && p.aborted {
441+
break
442+
}
443+
d.fn = nil
444+
// These args are just a copy, so can be cleared immediately
445+
memclrNoHeapPointers(deferArgs, uintptr(argWidth))
446+
if d._panic != nil && d._panic.recovered {
447+
done = deferBits == 0
448+
break
449+
}
450+
}
451+
452+
return done
453+
454+
```
455+
308456
参考资料:
309457
310458
1. [open coded defer](https://github.com/golang/proposal/blob/master/design/34481-opencoded-defers.md)

0 commit comments

Comments
 (0)