Go語言編譯原理之變量捕獲

前言

在前邊的幾篇文章中已經基本分享完瞭編譯器前端的一些工作,後邊的幾篇主要是關於編譯器對抽象語法樹進行分析和重構,然後完成一系列的優化,其中包括以下五個部分:

  • 變量捕獲
  • 函數內聯
  • 逃逸分析
  • 閉包重寫
  • 遍歷函數

後邊的五篇文章主要就是上邊這五個主題,本文分享的是變量捕獲,變量捕獲主要是針對閉包場景的,因為閉包函數中可能引用閉包外的變量,因此變量捕獲需要明確在閉包中通過值引用或地址引用的方式來捕獲變量

變量捕獲概述

下邊通過一個示例來看一下什麼是變量捕獲

package main
import (
	"fmt"
)
func main() {
	a := 1
	b := 2
	go func() {
		//在閉包裡對a或b進行瞭重新賦值,也會改變引用方式
		fmt.Println(a, b)
	}()
	a = 666
}

我們可以看到在閉包中引用瞭外部的變量a、b,由於變量a在閉包之後進行瞭其他賦值操作,因此在閉包中,a、b變量的引用方式會有所不同。在閉包中,必須采取地址引用的方式對變量a進行操作,而對變量b的引用將通過直接值傳遞的方式進行

我們可以通過如下方式查看當前程序閉包變量捕獲的情況

go tool compile -m=2 main.go | grep capturing

assign=true代表變量a在閉包完成後又進行瞭賦值操作

也可以看一個稍微復雜的

func adder() func(int) int {//累加器
	sum := 0 //地址引用
	return func(v int) int {
		sum += v
		return sum
	}
}
func main() {
	a := adder()
	for i:=0;i<10;i++{
		fmt.Printf("0 + 1 + ... + %d = %d\n", i, a(i))
	}
}

上一篇文章分享瞭類型檢查,我們可以繼續順著編譯的入口文件中類型檢查後邊的代碼往下看,你會看到如下這段代碼

編譯入口文件:src/cmd/compile/main.go -> gc.Main(archInit)
// Phase 4: Decide how to capture closed variables.(決定如何捕獲閉包變量)
// This needs to run before escape analysis,
// because variables captured by value do not escape.(變量捕獲應該在逃逸分析之前進行,因為值類型的變量捕獲,不會進行逃逸分析)
	timings.Start("fe", "capturevars")
	for _, n := range xtop {
		if n.Op == ODCLFUNC && n.Func.Closure != nil { //函數需要是閉包類型
			Curfn = n
			capturevars(n)
		}
	}
	capturevarscomplete = true

從上邊這段代碼及註釋中,我們可以得到以下幾個信息:

  • 變量捕獲應該在逃逸分析之前進行,因為值類型的變量捕獲,不會進行逃逸分析
  • 變量捕獲是針對閉包函數的
  • 變量捕獲的實現主要是調用瞭:src/cmd/compile/internal/gc/closure.go→capturevars

下邊我們就去看capturevars方法的內部實現,瞭解變量捕獲的一些細節

變量捕獲底層實現

所有類型檢查完成後,capturevars將在單獨的階段調用,它決定閉包捕獲的每個變量是通過值還是通過引用捕獲

func capturevars(xfunc *Node) {
	......
	clo := xfunc.Func.Closure
	cvars := xfunc.Func.Cvars.Slice()
	out := cvars[:0]
	for _, v := range cvars {
		......
		out = append(out, v)
		......
		outer := v.Name.Param.Outer
		outermost := v.Name.Defn
		// out parameters will be assigned to implicitly upon return.
		if outermost.Class() != PPARAMOUT && !outermost.Name.Addrtaken() && !outermost.Name.Assigned() && v.Type.Width <= 128 {
			v.Name.SetByval(true)
		} else {
			outermost.Name.SetAddrtaken(true)
			outer = nod(OADDR, outer, nil)
		}
		......
		outer = typecheck(outer, ctxExpr)
		clo.Func.Enter.Append(outer)
	}
	xfunc.Func.Cvars.Set(out)
	lineno = lno
}

該方法的代碼量很少,大致內容就是,它會先獲取到閉包函數內所有變量節點,然後對這些節點進行遍歷。確定該閉包需要捕獲的變量之後再沒有被修改時,該變量小於128字節,則會認為他是值引用。後邊它會對外部引用的結點進行類型檢查

總結

本部分比較簡單,但是挺實用的,特別是我這種一直搞不明包閉包引用外部變量的人。後邊的逃逸分析、閉包重寫跟變量捕獲有一定的聯系,介紹的後邊內容的時候再提

以上就是Go語言編譯原理之變量捕獲的詳細內容,更多關於Go編譯原理變量捕獲的資料請關註WalkonNet其它相關文章!

推薦閱讀: