go cmd/compile: expose what contributes to consuming the inlining budget

lawou6xi  于 4个月前  发布在  Go
关注(0)|答案(3)|浏览(95)

提案详情

将导致内联预算消耗的因素作为调试标志进行暴露

有时候,理解一个函数为何被内联或者没有被内联以及该函数预算消耗的主要贡献者是什么并不容易。
本提案的目的是在 go tool compile 命令中添加一个 -d 标志,以启用预算消耗的跟踪。

概念验证

我已经有一个实现这一功能的POC(Proof of Concept)和一个利用该POC提供摘要的工具。
这是一个示例执行过程:

> go build -gcflags "-d=inlcostreason" hello.go
./hello.go:24:6: inline-function-cost: 123
./hello.go:24:6: inline-function-cost: 0,not inlining too expensive
./hello.go:25:13: inline-cost: 57,non-leaf inlining,0
./hello.go:25:13: inline-cost: 1,node,1
./hello.go:25:13: inline-cost: 1,node,2
./hello.go:25:13: inline-cost: 1,node,6
./hello.go:25:16: inline-cost: 1,node,3
./hello.go:25:16: inline-cost: 1,node,4
./hello.go:25:25: inline-cost: 1,node,5
./hello.go:26:20: inline-cost: 59,inlined function body,7
./hello.go:26:20: inline-cost: 1,node,7
...

这个文件,通过我构建的一个工具传递给你,给出了这样的输出(仅显示部分输出):

...
func usage() {  //  123, total inline cost, not inlining too expensive
	fmt.Fprintf(os.Stderr, "usage: helloserver [options]\n")  //  63, non-leaf inlining, node
	flag.PrintDefaults()  //  60, inlined function body, node
	os.Exit(2)
}
...

为了理解我所做的,我在正确的位置(预算发生变化的地方)添加了类似这样的代码块:

if base.Debug.InlCostReason != 0 {
    base.WarnfAt(v.lastFunctionPos, "inline-cost: %d,%s,%d", inlineExtraThrowCost, "trow", v.counter)
    v.counter++
}

计数器避免了行重复,而 lastFunctionPos 避免了将已经可内联的代码作为其他函数的一部分显示。但这些都是我们需要在以后确定的最佳方法的实现细节。

5f0d552i

5f0d552i2#

你好,@jespino
感谢你提出这个问题。
我将移除提案标签,因为这种类型的更改涉及到编译器实现,而不是添加新的API或对语言进行修改。对于这些类型的更改,没有必要经过繁琐的提案审查过程(这包括委员会会议、长时间的领先时间等)。
关于具体细节:我对一个新的调试选项感到满意,但有两件事需要记住:
首先,我们(Go编译器团队)一直在不断更改编译器的内部IR和转换/阶段,这可能包括内联器如何评估函数以决定它们是否是内联候选者。某个函数中的某个IR构造在一个Go版本中可能评估为6,然后在下一个版本中评估为7或3,这是因为内联器或解析器的更改或其他阶段的更改。这里的含义是Go用户不应该依赖任何内联预算调试工具报告的具体数字。
第二件事是我们正在积极将内联器从一个模型中移除,该模型将内联性作为给定函数的属性来决定(我喜欢将其称为“在所有地方内联F或在任何地方都不内联F”的方案),并朝着考虑特定调用站点属性的模型发展(这意味着函数F可能在某些调用站点被内联,而在其他调用站点不被内联)。这里的意图是鼓励“生产性的”内联,并阻止仅仅有助于代码膨胀的内联。以下是一些用Go编写的伪代码来说明:

package x

import (
	"fmt"
	"os"
)

func Y(x int, q []int) int {
	if len(q) == 0 {
		return x
	}
	v := 0
	for i := range q {
		v += x ^ i
	}
	return v
}

func hasSomeCalls(q, r int, z [][]int) int {
	if q < 0 {
		fmt.Fprintf(os.Stderr, "with Y reporting %d, unexpected bad q value",
			Y(q, z[0]))
		os.Exit(1)
	}
	v := 0
	for i := 0; i < 100000; i++ {
		v += Y(r, z[i&3])
	}
	return v
}

上面有两个对“Y”的调用。第一个调用发生在程序终止之前的错误路径上。在这个调用处内联“Y”不会对性能产生任何有意义的影响,它只会使二进制文件变得更臃肿。另一方面,第二个对“Y”的调用发生在我们知道会执行很多次的循环中。
我们正在尝试以一种方式更改编译器,以便在第一个调用站点处阻止内联并在第二个调用站点处鼓励内联。为了做到这一点,我们需要远离现有的方案,只需估算函数的大小并根据大小截止点声明它是“始终内联”还是“从未内联”。
我们正在对内联器所做的更改是通过启用GOEXPERIMENT=newinliner;这是一个活动开发分支,因此新事物正不断添加到其中。
考虑到这一点,我认为添加某种“-d”标志是可以接受的,我喜欢在注解中生成带有内联信息的Annotate Go源文件的想法。如果你们的工具可以扩展到GOEXPERIMENT=newinliner努力结束时我们想到的东西也是很好的。

v1uwarro

v1uwarro3#

感谢@thanm,这太棒了。我会深入研究新的内联器代码,看看是否仍然有意义去追踪到底是什么导致它没有被内联,以及如何暴露它。如果没有一种容易追踪的方法来实现它,也许追踪它就没有意义。但让我调查一下,我会回来扩展或修改这个想法。

相关问题