Go编译原理系列11(闭包重写)

145 阅读3分钟

前言

在上一篇文章中分享了编译优化阶段的逃逸分析,本文分享倒数第二个优化阶段—-闭包重写。在编译优化的第一个阶段分享了变量捕获它用于决定是通过指针引用还是值引用的方式传递外部变量。本文分享的闭包重写,分为闭包定义后被立即调用闭包定义后不被立即调用两种情况。下边会针对这两种情况进行概述和源码的分析

闭包重写概述

闭包被立即调用

在闭包被立即调用的情况下,闭包只能被调用一次,这时可以将闭包转换为普通函数的调用形式

func do(){
	a := 666
	func() {
		fmt.Println(a)
		a = 888
	}()
}

对于上边这个do函数中的闭包函数,根据变量捕获,我们知道,变量a是通过引用传递的方式传递到闭包中的。所以对于上边这个闭包函数,它经过闭包重写之后,就会被转换成类似正常函数调用的形式

func do(){
	a := 666
	func1(&a)
}

func func1(a *int) {
	fmt.Println(a)
	*a = 888
}

如果变量是值引用的,那么构造的新的函数参数应该为int类型

闭包不被立即调用

如果闭包定义后不被立即调用,而是后续调用,那么同一个闭包可能被调用多次,这时需要创建闭包对象

如果变量是按值引用的,并且该变量占用的存储空间小于2×sizeof(int),那么通过在函数体内创建局部变量的形式来产生该变量。如果变量通过指针或值引用,但是占用存储空间较大,那么捕获的变量(var)转换成指针类型的“&var”。这两种方式都需要在函数序言阶段将变量初始化为捕获变量的值

闭包重写底层实现

首先还是在反复提到的Go编译入口文件中找到如下这段代码

Go编译入口文件:src/cmd/compile/main.go -> gc.Main(archInit)

//Phase 7: Transform closure bodies to properly reference captured variables. 转换闭包体以正确引用捕获的变量
for _, n := range xtop {
		if n.Op == ODCLFUNC && n.Func.Closure != nil {
			Curfn = n
			transformclosure(n)
		}
}

遍历每一棵声明语句的抽象语法树,取到闭包函数声明的抽象语法树,并调用transformclosure进行闭包重写的操作。该方法会转换闭包体,来正确引用捕获的变量

transformclosure的实现比较简单,下边直接看它的内部实现(我会在下边必要的地方加上注释)

func transformclosure(xfunc *Node) {
	......
	clo := xfunc.Func.Closure

	if clo.Func.Top&ctxCallee != 0 { //如果是直接调用闭包,将其转换为普通函数调用,并将变量作为参数传递
		f := xfunc.Func.Nname

		var params []*types.Field//我们将在输入参数之前插入捕获的变量
		var decls []*Node
		for _, v := range xfunc.Func.Cvars.Slice() { //遍历闭包中引用的每一个变量
			if !v.Name.Byval() { // 变量是通过值还是通过引用捕获的
				addr := newname(lookup("&" + v.Sym.Name)) //创建一个新的ONAME Node
				addr.Type = types.NewPtr(v.Type)
				v.Name.Param.Heapaddr = addr
				v = addr
			}

			v.SetClass(PPARAM)
			decls = append(decls, v)

			fld := types.NewField()
			fld.Nname = asTypesNode(v)
			fld.Type = v.Type
			fld.Sym = v.Sym
			params = append(params, fld)
		}

		if len(params) > 0 {
			// 将闭包引用的参数作为普通函数的参数,加入到相应的抽象语法树中
			f.Type.Params().SetFields(append(params, f.Type.Params().FieldSlice()...))
			xfunc.Func.Dcl = append(decls, xfunc.Func.Dcl...)
		}

		dowidth(f.Type)
		xfunc.Type = f.Type // update type of ODCLFUNC
	} else {// 闭包没有被立即调用,所以它将保持为闭包对象
		var body []*Node
		offset := int64(Widthptr)
		for _, v := range xfunc.Func.Cvars.Slice() {
			cv := nod(OCLOSUREVAR, nil, nil)

			cv.Type = v.Type
			if !v.Name.Byval() {
				cv.Type = types.NewPtr(v.Type)
			}
			offset = Rnd(offset, int64(cv.Type.Align))
			cv.Xoffset = offset
			offset += cv.Type.Width

			//判断变量占用的存储空间大小,是否小于等于int64(2*Widthptr)
			if v.Name.Byval() && v.Type.Width <= int64(2*Widthptr) {
				v.SetClass(PAUTO)
				xfunc.Func.Dcl = append(xfunc.Func.Dcl, v)
				body = append(body, nod(OAS, v, cv))
			} else {
				// 声明包含从闭包中获取的地址的变量,并在函数序言阶段初始化
				addr := newname(lookup("&" + v.Sym.Name))
				addr.Type = types.NewPtr(v.Type)
				addr.SetClass(PAUTO)
				addr.Name.SetUsed(true)
				addr.Name.Curfn = xfunc
				xfunc.Func.Dcl = append(xfunc.Func.Dcl, addr)
				v.Name.Param.Heapaddr = addr
				if v.Name.Byval() {
					cv = nod(OADDR, cv, nil)
				}
				body = append(body, nod(OAS, addr, cv))
			}
		}

		if len(body) > 0 {
			typecheckslice(body, ctxStmt)
			xfunc.Func.Enter.Set(body)
			xfunc.Func.SetNeedctxt(true)
		}
	}

	lineno = lno
}

以上就是闭包重写的内容,可以大致了解一下它整个过程中都在做什么事情