关于后端:Go编译原理系列8变量捕获

4次阅读

共计 2078 个字符,预计需要花费 6 分钟才能阅读完成。

前言

在前边的几篇文章中曾经根本分享完了编译器前端的一些工作,后边的几篇次要是对于编译器对形象语法树进行剖析和重构,而后实现一系列的优化,其中包含以下五个局部:

  • 变量捕捉
  • 函数内联
  • 逃逸剖析
  • 闭包重写
  • 遍历函数

后边的五篇文章次要就是上边这五个主题,本文分享的是变量捕捉,变量捕捉次要是针对闭包场景的,因为闭包函数中可能援用闭包外的变量,因而变量捕捉须要明确在闭包中通过值援用或地址援用的形式来捕捉变量

变量捕捉概述

下边通过一个示例来看一下什么是变量捕捉

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

从上边这段代码及正文中,咱们能够失去以下几个信息:

  1. 变量捕捉应该在逃逸剖析之前进行,因为值类型的变量捕捉,不会进行逃逸剖析
  2. 变量捕捉是针对闭包函数的
  3. 变量捕捉的实现次要是调用了: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 字节,则会认为他是 值援用。后边它会对外部援用的结点进行类型查看

总结

本局部比较简单,然而挺实用的,特地是我这种始终搞不明包闭包援用内部变量的人。后边的逃逸剖析、闭包重写跟变量捕捉有肯定的分割,介绍的后边内容的时候再提

正文完
 0