首頁 > 軟體

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

2022-08-04 22:05:22

前言

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

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

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

變數捕獲概述

下邊通過一個範例來看一下什麼是變數捕獲

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 = %dn", 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編譯原理變數捕獲的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com