6.4 赋值语句

对于全局变量的赋值语句,编译器需要确定其初始化的顺序及初始化时间,我们分别加以讨论。

初始化顺序

确定初始化顺序的逻辑在文件cmd/compile/internal/pkginit/initorder.go中,入口函数是initOrder(), 该函数在总体逻辑中的 Step 2 中被调用。

影响初始化顺序的因素有两个:一是依赖关系、二是申明顺序。例如如下代码:

var nameAlias string = name + "!"
var name string = getDefaultName()
var version string = getDefaultVersion()

func getDefaultName() string {
	return "Golang"
}

func getDefaultVersion() string {
	return "1.17"
}

其中 nameAlias 依赖于 name, 所以即使它声明在 name 前面,其也应该在 name 之后进行初始化;但 nameversion 没有依赖关系,编译器会根据二者的申明顺序确定初始化顺序。

我们先来探索对依赖关系的处理方式。总体思路如下:为赋值语句确定三种状态:NotStarted, Pending, Done. 对于 Pending 状态中的语句,编译器记录两方面的信息:

  1. 该语句所依赖的其它变量的数目,记为 order. order = 0 表示该语句不依赖任何其他变量

  2. 依赖于该赋值语句的其他赋值语句列表,记为 blocking map. 对于赋值语句 A, blocking[A] 表示所有依赖 A 的赋值语句构成的列表

例如如下代码:

var x = f(a, b, b) // 记为 AssignA
var a = g()        // 记为 AssignB
var b = h()        // 记为 AssignC

AssignA 依赖变量 a, b, 所以 order[AssignA] = 2, 并且 blocking[AssignB] = [AssignA], blocking[AssignC] = [AssignA].

有了上述信息,处理步骤如下:

  1. 对于 NotStarted 的赋值语句,计算其 order 值与 blocking 列表,并将其标记为 Pending

  2. 对于所有 order=0 的 Pending 语句,将其放入一个 ready queue 中,标记为 Done 并将其 blocking 列表中的所有语句的 order 值减一

  3. 重复第二步直到处理完所有 Pending 语句

处理逻辑定义在函数 initOrder() 中,删掉错误检测相关的代码,核心逻辑如下:

func initOrder(l []ir.Node) []ir.Node {
    s := staticinit.Schedule{
        Plans: make(map[ir.Node]*staticinit.Plan),
        Temps: make(map[ir.Node]*ir.Name),
    }
    o := InitOrder{
        blocking: make(map[ir.Node][]ir.Node),
        order:    make(map[ir.Node]int),
    }

    // Process all package-level assignment in declaration order.
    for _, n := range l {
        switch n.Op() {
        case ir.OAS, ir.OAS2DOTTYPE, ir.OAS2FUNC, ir.OAS2MAPR, ir.OAS2RECV:
            o.processAssign(n)
            o.flushReady(s.StaticInit)
        case ir.ODCLCONST, ir.ODCLFUNC, ir.ODCLTYPE:
            // nop
        default:
            base.Fatalf("unexpected package-level statement: %v", n)
        }
    }

    return s.Out
}

其中 staticinit.Schedule 用来判定初始化时间,将会在下一节讨论。结构体 InitOrder 用来保存 order 及 blocking 列表的信息,定义如下:

type InitOrder struct {
    // blocking list
    blocking map[ir.Node][]ir.Node
    // ready queue
    ready declOrder
    order map[ir.Node]int
}

函数的参数是 typecheck.Target.Decls, for 循环用来遍历所有 NotStarted 状态的赋值语句,并使用方法 processAssign() 来计算 order 与 blocking 属性,该方法代码如下:

func (o *InitOrder) processAssign(n ir.Node) {
    if _, ok := o.order[n]; ok {
        base.Fatalf("unexpected state: %v, %v", n, o.order[n])
    }
    o.order[n] = 0

    // 通过 collectDeps() 函数解析依赖列表
    for dep := range collectDeps(n, true) {
        defn := dep.Defn
        // Skip dependencies on functions (PFUNC) and
        // variables already initialized (InitDone).
        if dep.Class != ir.PEXTERN || o.order[defn] == orderDone {
            continue
        }
        // 设置 order 与 blocking 列表
        o.order[n]++
        o.blocking[defn] = append(o.blocking[defn], n)
    }

    // 如果该节点没有依赖,则放入 ready queue 中
    if o.order[n] == 0 {
        heap.Push(&o.ready, n)
    }
}

flushReady() 处理所有 order=0 的语句并修改对应的 blocking 列表, flushReady() 会使用方法 s.StaticInit() 进行静态初始化,如果赋值语句必须在运行时进行,则将其保存在 s.Out 中,该属性是方法的返回值。方法的代码如下:

func (o *InitOrder) flushReady(initialize func(ir.Node)) {
    for o.ready.Len() != 0 {
        n := heap.Pop(&o.ready).(ir.Node)
        if order, ok := o.order[n]; !ok || order != 0 {
            base.Fatalf("unexpected state: %v, %v, %v", n, ok, order)
        }

        initialize(n) // 使用方法 StaticInit 进行处理
        o.order[n] = orderDone

        blocked := o.blocking[n]
        delete(o.blocking, n)

        // 修改 blocking 列表, 并将 order 降为 0 的语句推入 ready queue
        for _, m := range blocked {
            if o.order[m]--; o.order[m] == 0 {
                heap.Push(&o.ready, m)
            }
        }
    }
}

如果 flushReady() 在修改 blocking 列表有多个语句的 order 值都降为了0, 那么此时如何确定其顺序呢?该顺序通过最低优先级队列来实现,类型 declOrder 实现了 heap 接口:

type declOrder []ir.Node

func (s declOrder) Len() int { return len(s) }
func (s declOrder) Less(i, j int) bool {
    return firstLHS(s[i]).Pos().Before(firstLHS(s[j]).Pos())
}
func (s declOrder) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

func (s *declOrder) Push(x interface{}) { *s = append(*s, x.(ir.Node)) }
func (s *declOrder) Pop() interface{} {
    n := (*s)[len(*s)-1]
    *s = (*s)[:len(*s)-1]
    return n
}

这里的 Less 方法定义了两个语句的先后顺序:按照语句在代码中的位置进行排序,如果语句 A 在代码中排在 B 前面,那么就先对 A 进行初始化,再对 B 进行初始化。

关于堆的实现可以参考 container/heap 中的实现,这里不再展开。

初始化时间

对于已经加入 ready queue 中的赋值语句,编译器需要判断何时对其进行初始化,是需要推迟到运行时进行,还是在编译时便可完成。这个工作由定义在文件 cmd/compile/internal/staticinit/sched.go 中,入口函数是结构体 Schedule 的方法 StaticInit(), 该方法作为 flushReady() 的参数,在处理 ready queue 时使用。

如下代码反映了该部分的总体逻辑:

type Schedule struct {
    // 保存需要运行时才能初始化的语句,已排序
    Out []ir.Node

    Plans map[ir.Node]*Plan
    Temps map[ir.Node]*ir.Name
}

func (s *Schedule) append(n ir.Node) {
    s.Out = append(s.Out, n)
}

func (s *Schedule) StaticInit(n ir.Node) {
    if !s.tryStaticInit(n) {
        if base.Flag.Percent != 0 {
            ir.Dump("nonstatic", n)
        }
        s.append(n)
    }
}

方法 tryStaticInit() 会尝试着对赋值语句进行静态初始化,如果返回值为 false, 则说明该语句必须在运行时执行,程序会将其加入到列表 Schedule.Out 中,如果回顾 initOrder() 函数的话,可以发现该属性便是最终的返回值。

那么哪些语句的初始化可以在编译时完成呢?总体来说,如果右侧表达式是字面量初始化或者简单的类型转换,那么那么赋值操作便可以在编译时进行。对右侧表达式进行判断的逻辑都在方法 Schedule.StaticAssign() 中,此处不再详细展开,我们可以通过下一节介绍的方法查看 Dump 信息或者调试程序。

最后更新于