在阅读 Go runtime 相关的代码时, 我们可以看到大量 write barrier 相关的代码或者注释, 大概可以猜到是和 GC 相关的, 具体用途和原理之前确并不知晓.
Go 的垃圾回收可以被简化为两个步骤: 标记(mark)和清除(sweep).
GC 在标记阶段并不暂停所有协程的执行. 这就需要我们考虑, 如果对象的引用关系在标记阶段被修改了应该怎么办?
假设:
由于 C 仅被 A 引用, 但 A 已被标记为黑色, 所以 GC 不会再去标记 C. 在随后的清除阶段, 虽然 C 依然被引用, 但是会因为未被标记而被 GC 的回收, 这显然时不可接受的.
为了处理这种情况, Go 引入 write barrier, 即由编译器在需要的地方插入相关代码处理.
我们构造一个相关的例子:
var sink *int
func main() {
foo := []int{1, 2, 3}
sink = &foo[1]
}
在生成的汇编代码中我们可以找到相关内容:
cat -n objdump | grep -A 100 "main.main>:"
...
138038 sink = &foo[1]
138039 4576b1: 48 8d 48 08 lea 0x8(%rax),%rcx
138040 4576b5: 83 3d d4 10 0a 00 00 cmpl $0x0,0xa10d4(%rip) # 4f8790 <runtime.writeBarrier>
138041 4576bc: 74 15 je 4576d3 <main.main+0x53>
138042 4576be: 66 90 xchg %ax,%ax
138043 4576c0: e8 1b d2 ff ff call 4548e0 <runtime.gcWriteBarrier2>
138044 4576c5: 49 89 0b mov %rcx,(%r11)
138045 4576c8: 48 8b 05 51 32 07 00 mov 0x73251(%rip),%rax # 4ca920 <main.sink>
138046 4576cf: 49 89 43 08 mov %rax,0x8(%r11)
138047 4576d3: 48 89 0d 46 32 07 00 mov %rcx,0x73246(%rip) # 4ca920 <main.sink>
...
其逻辑是:
通过全局变量 runtime.writeBarrier 判断是否开启了 write barrier
runtime.writeBarrier 是一个全局变量, 在进入标记段前开启, 进入清除阶段前关闭.
如果开启了, 则调用 runtime.gcWriteBarrier2 将对象保存到当前的 p, GMP 中的 p
runtime.gcWriteBarrier2 是直接以会编实现的函数, 会将寄存器 AX 内的指针保存到 p.wbbuf.
标记阶段结束时, GC 会额外处理这些对象
在结束标记前, GC 会为调用 wbBufFlush 处理这缓存的对象.
Link: https://github.com/j2gg0s/j2gg0s/blob/main/_posts/2023-11-01-Go%20Runtime%3A%20WriteBarrier.md
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.