标签 Compiler 下的文章

Go 1.20中值得关注的几个变化

本文永久链接 – https://tonybai.com/2023/02/08/some-changes-in-go-1-20

美国时间2023年2月1日,唯一尚未退休的Go语言之父Robert Griesemer代表Go核心开发团队在Go官博撰文正式发布了Go 1.20版本。就像Russ Cox在2022 GopherCon大会所说的那样:Go2永不会到来,Go 1.x.y将无限延续

注:似乎新兴编程语言都喜欢停留在1.x.y上无限延续,譬如已经演化到1.67版本的Rust^_^。

《Go,13周年》之后,Go 1.20新特性在开发主干冻结(2022.11)之前,我曾写过一篇《Go 1.20新特性前瞻》,对照着Go 1.20 milestone中内容,把我认为的主要特性和大家简单过了一遍,不过那时Go 1.20毕竟没有正式发布,前瞻肯定不够全面,某些具体的点与正式版本可能也有差异!现在Go 1.20版本正式发布了,其Release Notes也补充完整了,在这一篇中,我再来系统说说Go 1.20版本中值得关注的那些变化。对于在前瞻一文中详细介绍过的特性,这里不会再重复讲解了,大家参考前瞻一文中的内容即可。而对于其他一些特性,或是前瞻一文中着墨不多的特性,这里会挑重点展开说说。

按照惯例,我们依旧首先来看看Go语法层面都有哪些变化,这可能也是多数Gopher们最为关注的变化点。

一. 语法变化

Go秉持“大道至简”的理念,对Go语法特性向来是“不与时俱进”的。自从Go 1.18大刀阔斧的加入了泛型特性后,Go语法特性就又恢复到了之前的“新三年旧三年,缝缝补补又三年”的节奏。Go 1.20亦是如此啊!Release Notes说Go 1.20版本在语言方面包含了四点变化,但看了变化的内容后,我觉得真正的变化只有一个,其他的都是修修补补。

1. 切片到数组的转换

唯一算是真语法变化的特性是支持切片类型到数组类型(或数组类型的指针)的类型转换,这个特性在前瞻一文中系统讲过,这里就不赘述了,放个例子大家直观认知一下就可以了:

// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/lang/slice2arr.go

func slice2arrOK() {
    var sl = []int{1, 2, 3, 4, 5, 6, 7}
    var arr = [7]int(sl)
    var parr = (*[7]int)(sl)
    fmt.Println(sl)  // [1 2 3 4 5 6 7]
    fmt.Println(arr) // [1 2 3 4 5 6 7]
    sl[0] = 11
    fmt.Println(arr)  // [1 2 3 4 5 6 7]
    fmt.Println(parr) // &[11 2 3 4 5 6 7]
}

func slice2arrPanic() {
    var sl = []int{1, 2, 3, 4, 5, 6, 7}
    fmt.Println(sl)
    var arr = [8]int(sl) // panic: runtime error: cannot convert slice with length 7 to array or pointer to array with leng  th 8
    fmt.Println(arr)     // &[11 2 3 4 5 6 7]

}

func main() {
    slice2arrOK()
    slice2arrPanic()
}

有两点注意一下就好:

  • 切片转换为数组类型的指针,那么该指针将指向切片的底层数组,就如同上面例子中slice2arrOK的parr变量那样;
  • 转换的数组类型的长度不能大于原切片的长度(注意是长度而不是切片的容量哦),否则在运行时会抛出panic。

2. 其他的修修补补

  • comparable“放宽”了对泛型实参的限制

下面代码在Go 1.20版本之前,比如Go 1.19版本中会无法通过编译:

// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/lang/comparable.go

func doSth[T comparable](t T) {
}

func main() {
    n := 2
    var i interface{} = n // 编译错误:interface{} does not implement comparable
    doSth(i)
}

之前,comparable约束下的泛型形参需要支持严格可比较(strictly comparable)的类型作为泛型实参,哪些是严格可比较的类型呢?Go 1.20的语法规范做出了进一步澄清:如果一个类型是可比较的,且不是接口类型或由接口类型组成的类型,那么这个类型就是严格可比较的类型,包括:

- 布尔型、数值类型、字符串类型、指针类型和channel是严格可比较的。
- 如果结构体类型的所有字段的类型都是严格可比较的,那么该结构体类型就是严格可比较的。
- 如果数组元素的类型是严格可比较的,那么该数组类型就是严格可比较的。
- 如果类型形参的类型集合中的所有类型都是严格可比较的,那么该类型形参就是严格可比较的。

我们看到:例外的就是接口类型了。接口类型不是“严格可比较的(strictly comparable)”,但未作为类型形参的接口类型是可比较的(comparable),如果两个接口类型的动态类型相同且值相等,那么这两个接口类型就相等,或两个接口类型的值均为nil,它们也相等,否则不等。

Go 1.19版本及之前,作为非严格比较类型的接口类型是不能作为comparable约束的类型形参的类型实参的,就像上面comparable.go中示例代码那样,但Go 1.20版本开始,这一要求被防控,接口类型被允许作为类型实参赋值给comparable约束的类型形参了!不过这么做之前,你也要明确一点,如果像下面这样两个接口类型底层类型相同且是不可比较的类型(比如切片),那么代码将在运行时抛panic:

// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/lang/comparable1.go

func doSth[T comparable](t1, t2 T) {
    if t1 != t2 {
        println("unequal")
        return
    }
    println("equal")
}

func main() {
    n1 := []byte{2}
    n2 := []byte{3}
    var i interface{} = n1
    var j interface{} = n2
    doSth(i, j) // panic: runtime error: comparing uncomparable type []uint8
}

Go 1.20语言规范借此机会还进一步澄清了结构体和数组两种类型比较实现的规范:对于结构体类型,Go会按照结构体字段的声明顺序,逐一字段进行比较,直到遇到第一个不相等的字段为止。如果没有不相等字段,则两个结构体字段相等;对于数组类型,Go会按数组元素的顺序,逐一元素进行比较,直到遇到第一个不相等的元素为止。如果没有不相等的元素,则两个数组相等。

  • unsafe包继续添加“语法糖”

Go 1.17版本在unsafe包增加Slice函数后,Go 1.20版本又增加三个语法糖函数:SliceData、String和StringData:

// $GOROOT/src/unsafe/unsafe.go
func SliceData(slice []ArbitraryType) *ArbitraryType
func String(ptr *byte, len IntegerType) string
func StringData(str string) *byte

值得注意的是由于string的不可更改性,String函数的参数ptr指向的内容以及StringData返回的指针指向的内容在String调用和StringData调用后不允许修改,但实际情况是怎么样的呢?

// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/lang/unsafe.go

func main() {
    var arr = [6]byte{'h', 'e', 'l', 'l', 'o', '!'}
    s := unsafe.String(&arr[0], 6)
    fmt.Println(s) // hello!
    arr[0] = 'j'
    fmt.Println(s) // jello!

    b := unsafe.StringData(s)
    *b = 'k'
    fmt.Println(s) // kello!

    s1 := "golang"
    fmt.Println(s1) // golang
    b = unsafe.StringData(s1)
    *b = 'h' // fatal error: fault, unexpected fault address 0x10a67e5
    fmt.Println(s1)
}

我们看到:unsafe.String函数调用后,如果我们修改了传入的指针指向的内容,那么该改动会影响到后续返回的string内容!而StringData返回
的指针所指向的内容一旦被修改,其结果要根据字符串的来源而定了。对于由可修改的底层数组“创建”的字符串(如s),通过StringData返回的指
针可以“修改”字符串的内容;而对于由字符串字面值初始化的字符串变量(如s1),其内容是不可修改的(编译器将字符串底层存储分配在了只读数据区),尝试通过指针修改指向内容,会导致运行时的段错误。

二. 工具链

1. Go安装包“瘦身”

这些年,Go发布版的安装包“体格”是越来越壮了,动辄100多MB的压缩包,以go.dev/dl页面上的go1.xy.linux-amd64.tar.gz为例,我们看看从Go 1.15版本到Go 1.19版本的“体格”变化趋势:

Go 1.15 - 116MB
Go 1.16 - 123MB
Go 1.17 - 129MB
Go 1.18 - 135MB
Go 1.19 - 142MB

如果按此趋势,Go 1.20势必要上到150MB以上。但Go团队找到了“瘦身”方法,那就是:从Go 1.20开始发行版的安装包不再为GOROOT中的软件包提供预编译的.a文件了,这样我们得到的瘦身后的Go 1.20版本的size为95MB!相较于Go 1.19,Go 1.20的安装包“瘦”了三分之一。安装包解压后这种体现更为明显:

➜  /Users/tonybai/.bin/go1.19 git:(master) ✗ $du -sh
495M    .
➜  /Users/tonybai/.bin/go1.20 git:(master) ✗ $du -sh
265M    .

我们看到:Go 1.20占用的磁盘空间仅为Go 1.19版本的一半多一点而已。 并且,Go 1.20版本中,GOROOT下的源码将像其他用户包那样在构建后被缓存到本机cache中。此外,go install也不会为GOROOT下的软件包安装.a文件。

2. 编译器

1) PGO(profile-guided optimization)

Go 1.20编译器的一个最大的变更点是引入了PGO优化技术预览版,这个在前瞻一文中也有对PGO技术的简单介绍。说白了点,PGO技术就是在原有compiler优化技术的基础上,针对程序在生产环境运行中的热点关键路径再进行一轮优化,并且针对热点代码执行路径,编译器会放开一些限制,比如Go决定是否对函数进行内联优化的复杂度上限默认值是80,但对于PGO指示的关键热点路径,即便函数复杂性超过80很多,也可能会被inline优化掉。

之前持续性能剖析工具开发商Polar Signals曾发布一篇文章《Exploring Go’s Profile-Guided Optimizations》,专门探讨了PGO技术可能带来的优化效果,文章中借助了Go项目中自带的测试示例,这里也基于这个示例带大家重现一下。

我们使用的例子在Go 1.20源码/安装包的\$GOROOT/src/cmd/compile/internal/test/testdata/pgo/inline路径下:

$ls -l
total 3156
-rw-r--r-- 1 tonybai tonybai    1698 Jan 31 05:46 inline_hot.go
-rw-r--r-- 1 tonybai tonybai     843 Jan 31 05:46 inline_hot_test.go

我们首先执行一下inline目录下的测试,并生成用于测试的可执行文件以及对应的cpu profile文件供后续PGO优化使用:

$go test -o inline_hot.test -bench=. -cpuprofile inline_hot.pprof
goos: linux
goarch: amd64
pkg: cmd/compile/internal/test/testdata/pgo/inline
cpu: Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz
BenchmarkA-8        1348        870005 ns/op
PASS
ok      cmd/compile/internal/test/testdata/pgo/inline   1.413s

接下来,我们对比一下不使用PGO和使用PGO优化,Go编译器在内联优化上的区别:

$diff <(go test -run=none -tags='' -timeout=9m0s -gcflags="-m -m" 2>&1 | grep "can inline") <(go test -run=none -tags='' -timeout=9m0s -gcflags="-m -m -pgoprofile inline_hot.pprof" 2>&1 | grep "can inline")
4a5,6
> ./inline_hot.go:53:6: can inline (*BS).NS with cost 106 as: method(*BS) func(uint) (uint, bool) { x := int(i >> lWSize); if x >= len(b.s) { return 0, false }; w := b.s[x]; w = w >> (i & (wSize - 1)); if w != 0 { return i + T(w), true }; x = x + 1; for loop; return 0, false }
> ./inline_hot.go:74:6: can inline A with cost 312 as: func() { s := N(100000); for loop; for loop }

上面diff命令中为Go test命令传入-run=none -tags=”" -gcflags=”-m -m”是为了仅编译源文件,而不执行任何测试。

我们看到,相较于未使用PGO优化的结果,PGO优化后的结果多了两个inline函数,这两个可以被inline的函数,一个的复杂度开销为106,一个是312,都超出了默认的80,但仍然可以被inline。

我们来看看PGO的实际优化效果,我们分为在无PGO优化与有PGO优化下执行100次benchmark,再用benchstat工具对比两次的结果:

$go test -o inline_hot.test -bench=. -cpuprofile inline_hot.pprof -count=100 > without_pgo.txt
$go test -o inline_hot.test -bench=. -gcflags="-pgoprofile inline_hot.pprof" -count=100 > with_pgo.txt

$benchstat without_pgo.txt with_pgo.txt
goos: linux
goarch: amd64
pkg: cmd/compile/internal/test/testdata/pgo/inline
cpu: Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz
    │ without_pgo.txt │            with_pgo.txt             │
    │     sec/op      │   sec/op     vs base                │
A-8       874.7µ ± 0%   872.6µ ± 0%  -0.24% (p=0.024 n=100)

注:benchstat的安装方法:\$go install golang.org/x/perf/cmd/benchstat@latest

我们看到,在我的机器上(ubuntu 20.04 linux kerenel 5.4.0-132),PGO针对这个测试的优化效果并不明显(仅仅有0.24%的提升),Polar Signals原文中的提升幅度也不大,仅为1.05%。

Go官方Release Notes中提到benchmark提升效果为3%~4%,同时官方也提到了,这个仅仅是PGO初始技术预览版,后续会加强对PGO优化的投入,直至对多数程序产生较为明显的优化效果。个人觉得目前PGO尚处于早期,不建议在生产中使用。

Go官方也增加针对PGO的ref页面,大家重点看看其中的FAQ,你会有更多收获!

2) 构建速度

Go 1.18泛型落地后,Go编译器的编译速度出现了回退(幅度15%),Go 1.19编译速度也没有提升。虽然编译速度回退后依然可以“秒杀”竞争对手,但对于以编译速度快著称的Go来说,这个问题必须修复。Go 1.20做到了这一点,让Go编译器的编译速度重新回归到了Go 1.17的水准!相对Go 1.19提升10%左右。

我使用github.com/reviewdog/reviewdog这个库实测了一下,分别使用go 1.17.1、go 1.18.6、go 1.19.1和Go 1.20对这个module进行go build -a构建(之前将依赖包都下载本地,排除掉go get环节的影响),结果如下:

go 1.20:
$time go build -a github.com/reviewdog/reviewdog/cmd/reviewdog
go build -a github.com/reviewdog/reviewdog/cmd/reviewdog  48.01s user 7.96s system 536% cpu 10.433 total

go 1.19.1:
$time go build -a github.com/reviewdog/reviewdog/cmd/reviewdog
go build -a github.com/reviewdog/reviewdog/cmd/reviewdog  54.40s user 10.20s system 506% cpu 12.757 total

go 1.18.6:
$time go build -a github.com/reviewdog/reviewdog/cmd/reviewdog
go build -a github.com/reviewdog/reviewdog/cmd/reviewdog  53.78s user 9.85s system 545% cpu 11.654 total

go 1.17.1:
$time go build -a github.com/reviewdog/reviewdog/cmd/reviewdog
go build -a github.com/reviewdog/reviewdog/cmd/reviewdog  50.30s user 9.76s system 580% cpu 10.338 total

虽然不能十分精确,但总体上反映出各个版本的编译速度水准以及Go 1.20相对于Go 1.18和Go 1.19版本的提升。我们看到Go 1.20与Go 1.17版本在一个水平线上,甚至要超过Go 1.17(但可能仅限于我这个个例)。

3) 允许在泛型函数/方法中进行类型声明

Go 1.20版本之前下面代码是无法通过Go编译器的编译的:

// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/tools/compiler/local_type_decl.go
package main

func F[T1 any]() {
    type x struct{} // 编译错误:type declarations inside generic functions are not currently supported
    type y = x      // 编译错误:type declarations inside generic functions are not currently supported
}

func main() {
    F[int]()
}

Go 1.20改进了语言前端的实现,通过unified IR实现了对在泛型函数/方法中进行类型声明(包括定义type alias)的支持。

同时,Go 1.20在spec中还明确了哪些使用了递归方式声明的类型形参列表是不合法的

type T1[P T1[P]] …                    // 不合法: 形参列表中作为约束的T1引用了自己
type T2[P interface{ T2[int] }] …     // 不合法: 形参列表中作为约束的T2引用了自己
type T3[P interface{ m(T3[int])}] …   // 不合法: 形参列表中作为约束的T3引用了自己

type T4[P T5[P]] …                    // 不合法: 形参列表中,T4引用了T5 并且
type T5[P T4[P]] …                    //          T5引用了T4

type T6[P int] struct{ f *T6[P] }     // 正确: 虽然引用了T6,但这个引用发生在结构体定义中而不是形参列表中

4) 构建自举源码的Go编译器的版本选择

Go从Go 1.5版本开始实现自举,即使用Go实现Go,那么自举后的Go项目是谁来编译的呢?最初对应编译Go 1.5版本的Go编译器版本为Go 1.4。

以前从源码构建Go发行版,当未设置GOROOT_BOOTSTRAP时,编译脚本会默认使用Go 1.4,但如果有更高版本的Go编译器存在,会使用更高版本的编译器。

Go 1.18和Go 1.19会首先寻找是否有go 1.17版本,如果没有再使用go 1.4。

Go 1.20会寻找当前Go 1.17的最后一个版本Go 1.17.13,如果没有,则使用Go 1.4。

将来,Go核心团队计划一年升级一次构建自举源码的Go编译器的版本,例如:Go 1.22版本将使用Go 1.20版本的编译器。

5) cgo

Go命令现在在没有C工具链的系统上会默认禁用了cgo。更具体来说,当CGO_ENABLED环境变量未设置,CC环境变量未设置以及PATH环境变量中没有找到默认的C编译器(通常是clang或gcc)时,CGO_ENABLED会被默认设置为0。

3. 其他工具

1) 支持采集应用执行的代码盖率

在前瞻一文中,我提到过Go 1.20将对代码覆盖率的支持扩展到了应用整体层面,而不再仅仅是unit test。这里使用一个例子来看一下,究竟如何采集应用代码的执行覆盖率。我们以gitlab.com/esr/loccount这个代码统计工具为例,先修改一下Makefile,在go build后面加上-cover选项,然后编译loccount,并对其自身进行代码统计:

// /home/tonybai/go/src/gitlab.com/loccount
$make
$mkdir mycovdata
$GOCOVERDIR=./mycovdata loccount .
all          SLOC=4279    (100.00%) LLOC=1213    in 110 files
Go           SLOC=1724    (40.29%)  LLOC=835     in 3 files
asciidoc     SLOC=752     (17.57%)  LLOC=0       in 5 files
C            SLOC=278     (6.50%)   LLOC=8       in 2 files
Python       SLOC=156     (3.65%)   LLOC=0       in 2 files
... ...

上面执行loccount之前,我们建立了一个mycovdata目录,并设置GOCOVERDIR的值为mycovdata目录的路径。在这样的上下文下,执行loccount后,mycovdata目录下会生成一些覆盖率统计数据文件:

$ls mycovdata
covcounters.4ec45ce64f965e77563ecf011e110d4f.926594.1675678144659536943  covmeta.4ec45ce64f965e77563ecf011e110d4f

怎么查看loccount的执行覆盖率呢?我们使用go tool covdata来查看:

$go tool covdata percent -i=mycovdata
    loccount    coverage: 69.6% of statements

当然, covdata子命令还支持其他一些功能,大家可以自行查看manual挖掘。

2) vet

Go 1.20版本中,go工具链的vet子命令增加了两个十分实用的检测:

  • 对loopclosure这一检测策略进行了增强

具体可参见https://github.com/golang/tools/tree/master/go/analysis/passes/loopclosure代码

  • 增加对2006-02-01的时间格式的检查

注意我们使用time.Format或Parse时,最常使用的是2006-01-02这样的格式,即ISO 8601标准的时间格式,但一些代码中总是出现2006-02-01,十分容易导致错误。这个版本中,go vet将会对此种情况进行检查。

三. 运行时与标准库

1. 运行时(runtime)

Go 1.20运行时的调整并不大,仅对GC的内部数据结构进行了微调,这个调整可以获得最多2%的内存开销下降以及cpu性能提升。

2. 标准库

标准库肯定是变化最多的那部分。前瞻一文中对下面变化也做了详细介绍,这里不赘述了,大家可以翻看那篇文章细读:

  • 支持wrap multiple errors
  • time包新增DateTime、DateOnly和TimeOnly三个layout格式常量
  • 新增arena包
    … …

标准库变化很多,这里不能一一罗列,再补充一些我认为重要的,其他的变化大家可以到Go 1.20 Release Notes去看:

1) arena包

前瞻一文已经对arena包做了简要描述,对于arena包的使用以及最佳适用场合的探索还在进行中。著名持续性能剖析工具pyroscope的官方博客文章《Go 1.20 arenas实践:arena vs. 传统内存管理》对于arena实验特性的使用给出了几点好的建议,比如:

  • 只在关键的代码路径中使用arena,不要到处使用它们
  • 在使用arena之前和之后对你的代码进行profiling,以确保你在能提供最大好处的地方添加arena。
  • 密切关注arena上创建的对象的生命周期。确保你不会把它们泄露给你程序中的其他组件,因为那里的对象可能会超过arena的寿命。
  • 使用defer a.Free()来确保你不会忘记释放内存。
  • 如果你想在arena被释放后使用对象,使用arena.Clone()将其克隆回heap中。

pyroscope的开发人员认为arena是一个强大的工具,也支持标准库中保留arena这个特性,但也建议将arena和reflect、unsafe、cgo等一样纳入“不推荐”使用的包行列。这点我也是赞同的。我也在考虑如何基于arena改进我们产品的协议解析器的性能,有成果后,我也会将实践过程分享出来的。

2) 新增crypto/ecdh包

密码学包(crypto)的主要maintainer Filippo Valsorda从google离职后,成为了一名专职开源项目维护者。这似乎让其更有精力和动力对crypto包进行更好的规划、设计和实现了。crypto/ecdh包就是在他的提议下加入到Go标准库中的

相对于标准库之前存在的crypto/elliptic等包,crypto/ecdh包的API更为高级,Go官方推荐使用ecdh的高级API,这样大家以后可以不必再与低级的密码学函数斗争了。

3) HTTP ResponseController

以前HTTP handler的超时都是http服务器全局指定一个的:包括ReadTimeout和WriteTimeout。但有些时候,如果能在某个请求范围内支持这些超时(以及可能的其他选项)将非常有用。Damien Neil就创建了这个增加ResponseController的提案,下面是一个在HandlerFunc中使用ResponseController的例子:

http.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) {
  ctl := http.NewResponseController(w, r)
  ctl.SetWriteDeadline(time.Now().Add(1 * time.Minute)) // 仅为这个请求设置deadline
  fmt.Fprintln(w, "Hello, world.") // 这个写入的timeout为1-minute
})

4) context包增加WithCancelCause函数

context包新增了一个WithCancelCause函数,与WithCancel不同,通过WithCancelCause返回的Context,我们可以得到cancel的原因,比如下面示例:

// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/library/context.go

func main() {
    myError := fmt.Errorf("%s", "myError")
    ctx, cancel := context.WithCancelCause(context.Background())
    cancel(myError)
    fmt.Println(ctx.Err())          // context.Canceled
    fmt.Println(context.Cause(ctx)) // myError
}

我们看到通过context.Cause可以得到Context在cancel时传入的错误原因。

四. 移植性

Go对新cpu体系结构和OS的支持向来是走在前面的。Go 1.20还新增了对freebsd在risc-v上的实验性支持,其环境变量为GOOS=freebsd, GOARCH=riscv64。但Go 1.20也将成为对下面平台提供支持的最后一个Go版本:

  • Windows 7, 8, Server 2008和Server 2012
  • MacOS 10.13 High Sierra和10.14 (我的安装了10.14的mac os又要在go 1.21不被支持了^_^)

近期Go团队又有了新提案:支持WASI(GOOS=wasi GOARCH=wasm),WASI是啥,它是WebAssembly一套与引擎无关(engine-indepent)的、面向非Web系统的WASM API标准,是WebAssembly脱离浏览器的必经之路!一旦生成满足WASI的WASM程序,该程序就可以在任何支持WASI或兼容的runtime上运行。不出意外,该提案将在Go 1.21或Go 1.22版本落地。

本文中的示例代码可以在这里下载。


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2023年,Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码,关注代码质量并深入理解Go核心技术,并继续加强与星友的互动。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

通过实例理解Go静态单赋值(SSA)

本文永久链接 – https://tonybai.com/2022/10/21/understand-go-ssa-by-example

在上一篇文章《通过实例理解Go内联优化》中,我们探讨了Go编译器在编译中端进行的内联优化。内联优化基于IR中间表示进行,不过Go编译过程不止有一种IR表示,这点和龙书《编译原理(第二版)》的在第六章“中间代码生成”一开始处的讲解是一致的,即在将给定源语言的一个程序翻译成特定的目标机器代码的过程中,一个编译器可能构造出一系列中间表示(IR),如下图:

高层中间表示更接近于源语言,而低层的中间表示则更接近于目标机器。在Go编译过程中,如果说内联优化使用的IR是高层中间表示,那么低层中间表示非支持静态单赋值(SSA)的中间代码形式莫属。

在这一篇中,我们将沿着Go编译器的后端优化之路继续走下去,我们来认识一下静态单赋值(SSA)

1. 静态单赋值(SSA)的历史

静态单赋值(Static Single Assignment,SSA),也有称为Single Static Assignment的,是一种中间代码的表示形式(IR),或者说是某种中间代码所具备的属性,它是由IBM的三位研究员:Barry K. Rosen、Mark N. Wegman和F. Kenneth Zadeck于1988年提出的。

具有SSA属性的IR都具有这样的特征:

  • 每个变量在使用前都需要被定义
  • 每个变量被精确地赋值一次(使得一个变量的值与它在程序中的位置无关)

下面是一个简单的例子(伪代码):

y = 1
y = 2
x = y

转换为SSA形式为:

y1 = 1
y2 = 2
x1 = y2

我们看到由于SSA要求每个变量只能赋值一次,因此在转换为SSA后,变量y用y1和y2来表示,后面的序号越大,表明y的版本越新。从这一段三行的代码我们也可以看到,在SSA层面,y1 = 1这行代码就是一行死代码(dead code),即对结果不会产生影响的代码,可以在中间代码优化时被移除掉。

1991年,同样来自IBM研究院的Ron Cytron和Jeanne Ferrante以及前面的三位研究员又一起给出了构建SSA的快速算法,这进一步推动了SSA在编译器领域的快速应用。

SSA的提出以及后续的流行正是因为SSA形式中间代码具有很好的优化空间,基于SSA可以开启一些新的编译器优化算法或增强现有的优化算法,因此自SSA提出后,各种主流语言编译器后端均逐渐开始支持SSA,包括GCC、llvm、hotspot JVM、v8 js等。SSA也成为了一种IR表示的事实标准。

那么Go语言是何时开始与SSA结缘的呢?我们继续往下看。

2. Go与SSA

相对于GCC、LLVM,Go编译器还相对年轻,因此SSA加入Go的时间还不算太长。

Go SSA的工作始于Go 1.5版本实现自举之前,2015年2月初,负责编译器后端的Go团队核心成员的Keith Randall博士就在golang-dev google group上提出要让Go支持SSA的工作计划:

“我想从目前基于语法树的IR转换到更现代的基于SSA的IR。有了SSA IR,我们可以实现很多在当前编译器中难以做到的优化” - Keith Randall

同期,Keith Randall博士还编写了“New SSA Backend for the Go Compiler”文档,具体介绍了Go要支持SSA的理由以及分几步走的实现方案。

在为什么选择自己实现SSA IR,而不是转换为当时现成的诸如gcc, llvm等支持的IR形式并利用成熟后端进行中间代码优化这个问题上,Keith Randall博士给出了三点理由:

  • 从Go编译速度考虑:Go团队和社区对编译速度有着格外的青睐,Randall的目标是设计一个线性时间的SSA算法,实现快速SSA优化,但gcc, llmv等IR显然没有在速度方面给予额外的考虑;

  • 从功能完整性上考虑:Go运行时需要精确的栈帧地图(the map of stack frame),用来支持GC和栈拷贝,这些在gcc, llvm中都不会提供;

  • 从Go核心开发者的编译器使用体验方面考虑:如果使用llvm、gcc等ir,显然Go核心开发人员在编译go的时候还需要依赖llvm或gcc,这种额外的依赖对他们来说很难说是体验友好的。

2016年3月1日,在Go 1.7版本的master分支提交权限刚刚打开之后,Keith Randall就将支持ssa的dev.ssa分支合并到Go项目主线中了。

Go 1.7版本中,Go正式支持SSA,不过由于时间有限,Go 1.7 SSA仅支持针对amd64架构的优化。即便如此,Go支持SSA后,Keith Randall的benchmark显示性能提升12%,代码段缩小13%:


图:go 1.7 benchmark(图来自keith博士的slide)

Go 1.7正式发布时,其发布文档称Go程序的性能因对SSA的支持而提升5%-35%以上。由此看,Go SSA的实现达到了Keith Randall博士的预期目标,也为Go编译器后续的持续优化奠定了基础。

在2017年2月发布的Go 1.8版本中,Go SSA的支持范围扩展到其他所有Go支持的cpu架构,包括arm和arm64、mips和mips64、ppc64等。

了解了Go SSA的演进后,我们再来简单说说Go编译器中SSA的实现。

3. 转换为SSA

我们先来看看转换为SSA以及SSA优化在编译过程中所处的位置:


图:Go SSA所处的环节(图来自keith博士的slide)

上图是keith博士在2017年gophercon大会上slide中的一幅图,这幅图中明确了生成SSA形式以及SSA优化所处的环节。不过较新的Go版本中,convert to SSA之前也有一种不同于最初的抽象语法树的ir(比如:Go 1.19),SSA是由此种ir转换过来的。

从代码上来看,ir到SSA形式的转换发生在下面环节(Go 1.19版本代码,其他版本可能代码位置和内容均由不同):

// $GOROOT/src/cmd/compile/internal/gc/main.go
func Main(archInit func(*ssagen.ArchInfo)) {
    base.Timer.Start("fe", "init")

    defer handlePanic()

    archInit(&ssagen.Arch)
    ... ...

    // Compile top level functions.
    // Don't use range--walk can add functions to Target.Decls.
    base.Timer.Start("be", "compilefuncs")
    fcount := int64(0)
    for i := 0; i < len(typecheck.Target.Decls); i++ {
        if fn, ok := typecheck.Target.Decls[i].(*ir.Func); ok {
            // Don't try compiling dead hidden closure.
            if fn.IsDeadcodeClosure() {
                continue
            }
            enqueueFunc(fn)
            fcount++
        }
    }
    base.Timer.AddEvent(fcount, "funcs")

    compileFunctions()

    ... ...
}

在Main中,我们看到代码会将所有Target.Decls(函数)通过enqueueFunc入队列(compilequeue),然后调用compileFunctions来实现各个函数从AST ir到SSA形式的转换,compileFunctions在compile.go中,其实现如下:

// $GOROOT/src/cmd/compile/internal/gc/compile.go
func compileFunctions() {
    if len(compilequeue) == 0 {
        return
    }

    ... ...
    // By default, we perform work right away on the current goroutine
    // as the solo worker.
    queue := func(work func(int)) {
        work(0)
    }
    ... ...

    var compile func([]*ir.Func)
    compile = func(fns []*ir.Func) {
        wg.Add(len(fns))
        for _, fn := range fns {
            fn := fn
            queue(func(worker int) {
                ssagen.Compile(fn, worker)
                compile(fn.Closures)
                wg.Done()
            })
        }
    }
    types.CalcSizeDisabled = true // not safe to calculate sizes concurrently
    base.Ctxt.InParallel = true

    compile(compilequeue)
    ... ...
}

在compileFunctions中我们看到,编译器从compilequeue取出AST IR形式的函数,并调用ssagen.Compile将其编译为SSA形式。下面是ssagen.Compile的代码:

// $GOROOT/src/cmd/compile/internal/ssagen/pgen.go

// Compile builds an SSA backend function,
// uses it to generate a plist,
// and flushes that plist to machine code.
// worker indicates which of the backend workers is doing the processing.
func Compile(fn *ir.Func, worker int) {
    f := buildssa(fn, worker)
    // Note: check arg size to fix issue 25507.
    if f.Frontend().(*ssafn).stksize >= maxStackSize || f.OwnAux.ArgWidth() >= maxStackSize {
        largeStackFramesMu.Lock()
        largeStackFrames = append(largeStackFrames, largeStack{locals: f.Frontend().(*ssafn).stksize, args: f.OwnAux.ArgWidth(), pos: fn.Pos()})
        largeStackFramesMu.Unlock()
        return
    }
    pp := objw.NewProgs(fn, worker)
    defer pp.Free()
    genssa(f, pp)
    // Check frame size again.
    // The check above included only the space needed for local variables.
    // After genssa, the space needed includes local variables and the callee arg region.
    // We must do this check prior to calling pp.Flush.
    // If there are any oversized stack frames,
    // the assembler may emit inscrutable complaints about invalid instructions.
    if pp.Text.To.Offset >= maxStackSize {
        largeStackFramesMu.Lock()
        locals := f.Frontend().(*ssafn).stksize
        largeStackFrames = append(largeStackFrames, largeStack{locals: locals, args: f.OwnAux.ArgWidth(), callee: pp.Text.To.Offset - locals, pos: fn.Pos()})
        largeStackFramesMu.Unlock()
        return
    }

    pp.Flush() // assemble, fill in boilerplate, etc.
    // fieldtrack must be called after pp.Flush. See issue 20014.
    fieldtrack(pp.Text.From.Sym, fn.FieldTrack)
}

这里贴出了Compile的完整实现,Compile函数中真正负责生成具有SSA属性的中间代码的是buildssa函数,看了一下buildssa函数有近300行代码,有点复杂,这里挑挑拣拣,把主要的调用摘录出来:

// $GOROOT/src/cmd/compile/internal/ssagen/ssa.go

// buildssa builds an SSA function for fn.
// worker indicates which of the backend workers is doing the processing.
func buildssa(fn *ir.Func, worker int) *ssa.Func {
    name := ir.FuncName(fn)
    ... ...

    // Convert the AST-based IR to the SSA-based IR
    s.stmtList(fn.Enter)
    s.zeroResults()
    s.paramsToHeap()
    s.stmtList(fn.Body)

    // fallthrough to exit
    if s.curBlock != nil {
        s.pushLine(fn.Endlineno)
        s.exit()
        s.popLine()
    }
    ... ...

    // Main call to ssa package to compile function
    ssa.Compile(s.f)
    ... ...
}

buildssa中的ssa.Compile咱们后续再看,那个涉及到SSA的多轮(pass)优化,我们看一下从基于AST形式的IR到基于SSA形式的IR的转换,无论是fn.Enter还是fn.Body,本质都是一组ir Node,stmtList将这些node逐个转换为SSA形式。Go提供了可视化的ssa dump工具,我们可以更直观的来看一下。

Go语言隶属于命令式编程语言(imperative programming language),这类编程范式有三大典型控制结构:顺序结构、选择结构和循环结构,我们先来看看一个最简单的顺序结构是如何翻译为SSA的:

// github.com/bigwhite/experiments/tree/master/ssa-examples/sequential.go

package main

func sum(a, b, c int) int {
    d := a + b
    e := d + c
    return e
}

func main() {
    println(sum(1, 2, 3))
}

我们通过下面命令来生成函数sum的SSA转换过程:

$GOSSAFUNC=sum go build sequential.go
dumped SSA to ./ssa.html
$mv ssa.html ssa-sequential.html
$open ./ssa-sequential.html

上面的open命令会在本地打开浏览器并显示ssa-sequential.html页面:

上图中,最左侧是源码(源码显示两次,感觉是bug),中间的是AST形式的IR,最右侧的框框中就是Go编译器生成的第一版SSA,为了更好说明,我们将其贴到下面来:

// github.com/bigwhite/experiments/tree/master/ssa-examples/ssa-sequential.html

b1:-
  v1 (?) = InitMem <mem>
  v2 (?) = SP <uintptr>
  v3 (?) = SB <uintptr>
  v4 (?) = LocalAddr <*int> {a} v2 v1
  v5 (?) = LocalAddr <*int> {b} v2 v1
  v6 (?) = LocalAddr <*int> {c} v2 v1
  v7 (?) = LocalAddr <*int> {~r0} v2 v1
  v8 (3) = Arg <int> {a} (a[int])
  v9 (3) = Arg <int> {b} (b[int])
  v10 (3) = Arg <int> {c} (c[int])
  v11 (?) = Const64 <int> [0]
  v12 (+4) = Add64 <int> v8 v9 (d[int])
  v13 (+5) = Add64 <int> v12 v10 (e[int])
  v14 (+6) = MakeResult <int,mem> v13 v1
Ret v14 (+6)

name a[int]: v8
name b[int]: v9
name c[int]: v10
name d[int]: v12
name e[int]: v13

从结构上来看,SSA分为两部分,一部分是由b1、Ret组成的blocks,另一部分则是命名变量与SSA value的对应关系。

在SSA中,一个block代表了一个函数控制流图(control flow graph)中的基本代码块(basic block),从代码注释中可以看到SSA有四种block类型:Plain,If、Exit和Defer:

// $GOROOT/src/cmd/compile/internal/ssa/block.go

// BlockKind is the kind of SSA block.
//
//    kind          controls        successors
//  ------------------------------------------
//    Exit      [return mem]                []
//   Plain                []            [next]
//      If   [boolean Value]      [then, else]
//   Defer             [mem]  [nopanic, panic]  (control opcode should be OpStaticCall to runtime.deferproc)
type BlockKind int16

但实际的BlockKind已经与注释不一致了,opGen.go是一个自动生成的文件,其中的BlockKind类型的常量值有数十个,即便滤掉CPU架构相关的常量,剩下的还有8个(从BlockPlain到BlockFirst):

// $GOROOT/src/cmd/compile/internal/ssa/opGen.go

const (
    BlockInvalid BlockKind = iota
    ... ...

    BlockPlain
    BlockIf
    BlockDefer
    BlockRet
    BlockRetJmp
    BlockExit
    BlockJumpTable
    BlockFirst
)

上面的sum函数的SSA代码例子中,b1应该就是Plain类型的,Ret显然是BlockRet类型。

Plain类型的Block中是一组values,value是SSA的基本构成要素。根据SSA的定义,一个value只能被精确地定义一次,但是它可以被使用任意多次。如示例,一个value主要包括一个唯一的标识符,一个操作符,一个类型和一些参数,下面的Value类型的LongString和LongHTML方法返回的字符串更能说明Value的格式。尤其是LongHTML方法就是输出ssa html中内容的方法:

// $GOROOT/src/cmd/compile/internal/ssa/value.go

// long form print.  v# = opcode <type> [aux] args [: reg] (names)
func (v *Value) LongString() string {
    ... ...
}

// $GOROOT/src/cmd/compile/internal/ssa/html.go
func (v *Value) LongHTML() string {
    // TODO: Any intra-value formatting?
    // I'm wary of adding too much visual noise,
    // but a little bit might be valuable.
    // We already have visual noise in the form of punctuation
    // maybe we could replace some of that with formatting.
    s := fmt.Sprintf("<span class=\"%s ssa-long-value\">", v.String())

    linenumber := "<span class=\"no-line-number\">(?)</span>"
    if v.Pos.IsKnown() {
        linenumber = fmt.Sprintf("<span class=\"l%v line-number\">(%s)</span>", v.Pos.LineNumber(), v.Pos.LineNumberHTML())
    }

    s += fmt.Sprintf("%s %s = %s", v.HTML(), linenumber, v.Op.String())

    s += " &lt;" + html.EscapeString(v.Type.String()) + "&gt;"
    s += html.EscapeString(v.auxString())
    for _, a := range v.Args {
        s += fmt.Sprintf(" %s", a.HTML())
    }
    r := v.Block.Func.RegAlloc
    if int(v.ID) < len(r) && r[v.ID] != nil {
        s += " : " + html.EscapeString(r[v.ID].String())
    }
    var names []string
    for name, values := range v.Block.Func.NamedValues {
        for _, value := range values {
            if value == v {
                names = append(names, name.String())
                break // drop duplicates.
            }
        }
    }
    if len(names) != 0 {
        s += " (" + strings.Join(names, ", ") + ")"
    }

    s += "</span>"
    return s
}

以例子中的v12这一个value为例:

  v12 (+4) = Add64 <int> v8 v9 (d[int])
  • v12是该value的唯一标识符,其中的12为ID,ID是从1开始的整数;
  • (+4)是对应的源码的行号;
  • Add64是操作符;
  • 是value的类型(v.Type());
  • v8, v9则是Add64操作符的参数;
  • (d[int])是v12对应的LocalSlot,LocalSlot代表栈帧上的一个位置(location),用来识别和存储输出参数、输出参数或其他变量node。

ssa dump输出的另一部分则是命名变量与SSA value的对应关系,其格式也是:name LocalSlot: value:

name a[int]: v8
name b[int]: v9
name c[int]: v10
name d[int]: v12
name e[int]: v13

输出上述第二部分的代码如下:

// $GOROOT/src/cmd/compile/internal/ssa/print.go
func (p stringFuncPrinter) named(n LocalSlot, vals []*Value) {
    fmt.Fprintf(p.w, "name %s: %v\n", n, vals)
}

顺序结构的代码执行流是从上到下的,每个block后面仅有一个后继block,这样的SSA转换较为好理解。

下面我们再来看看一个选择控制结构 – if控制语句的ssa,下面是我们的示例Go源码:

// github.com/bigwhite/experiments/tree/master/ssa-examples/selection_if.go

package main

func foo(b bool) int {
    if b {
        return 2
    }
    return 3
}

func main() {
    println(foo(true))
}

我们通过下面命令输出函数foo的SSA中间代码:

$GOSSAFUNC=foo go build selection_if.go
dumped SSA to ./ssa.html
$mv ssa.html ssa-selection-if.html
$open ./ssa-selection-if.html

open命令启动浏览器显示foo函数的SSA形式:

有了上面关Go SSA格式的基础,这段SSA代码分析起来就容易一些了。

这段SSA中有多个block,包括plain block、if block、ret block等。我们重点关注SSA对if语句的处理。

经典SSA转换理论中,SSA将if分支转换为带有Φ函数的SSA代码(如下图):


图:if语句的SSA转换(图来自keith博士的slide)

Φ函数(希腊字母fài)是代码中的一个merge point,它可以将其前置的n个block的执行路径汇聚在一起。不过它仅用于代码分析使用,最终生成的代码中并不会有Φ函数的存在。关于在何处插入Φ函数等算法太理论了,这里就不展开了。

我们看看现实中go针对if语句的处理:

b1:
  v1 (?) = InitMem <mem>
  v2 (?) = SP <uintptr>
  v3 (?) = SB <uintptr>
  v4 (?) = LocalAddr <*bool> {b} v2 v1
  v5 (?) = LocalAddr <*int> {~r0} v2 v1
  v6 (3) = Arg <bool> {b} (b[bool])
  v7 (?) = Const64 <int> [0]
  v8 (?) = Const64 <int> [2]
  v11 (?) = Const64 <int> [3]
If v6 → b3 b2 (4)

b2: ← b1
  v13 (7) = Copy <mem> v1
  v12 (7) = MakeResult <int,mem> v11 v13
Ret v12 (+7)

b3: ← b1
  v10 (5) = Copy <mem> v1
  v9 (5) = MakeResult <int,mem> v8 v10
Ret v9 (+5)

name b[bool]: v6

这里关键是if block,if判断v6即变量b的值,如果为true,代码执行就流向block b3,否则流向block b2。

下面的b2、b3 block也都包含了前置block的属性,以b2为例,对于来自b1 block的流,执行对应block的代码。基于switch的选择语句更为复杂,有兴趣的朋友可以自己看一下ssa-selection-switch.html。

我们最后看一下循环结构,下面是Go代码:

// github.com/bigwhite/experiments/tree/master/ssa-examples/for_loop.go
package main

func sumN(n int) int {
    var r int
    for i := 1; i <= n; i++ {
        r = r + i
    }
    return r
}

func main() {
    println(sumN(10))
}

其生成的SSA如下图:

我们看到循环结构的ssa block更多,流向更为复杂,如果将其转换为一张图的话,那就应该是这样的:

我们看到:无论是选择结构还是循环结构,SSA实质上构建了一个函数的控制流图(control flow graph),图中每个节点就是一个block,函数的执行控制流在各个block间转移。而后续基于SSA的优化就是基于block中value的仅赋值一次的特性以及block的控制流图进行的

接下来,我们简单看看目前Go基于SSA IR都做了哪些优化。

4. 基于SSA的多轮(pass)优化

buildssa函数中ssa.Compile调用执行了基于SSA IR的多轮(passes)优化:

// $GOROOT/src/cmd/compile/internal/ssa/compile.go

func Compile(f *Func) {
    ... ...
    for _, p := range passes {
        ... ...
        tStart := time.Now()
        p.fn(f)
        tEnd := time.Now()
        ... ...
    }
}

我们看到,针对某个函数,Compile函数对其安装预置的passes进行多轮优化,都有哪些pass呢?我们来看看:

// $GOROOT/src/cmd/compile/internal/ssa/compile.go

// list of passes for the compiler
var passes = [...]pass{
    {name: "number lines", fn-3693: numberLines, required: true},
    {name: "early phielim", fn-3693: phielim},
    {name: "early copyelim", fn-3693: copyelim},
    {name: "early deadcode", fn-3693: deadcode}, // remove generated dead code to avoid doing pointless work during opt
    {name: "short circuit", fn-3693: shortcircuit},
    {name: "decompose user", fn-3693: decomposeUser, required: true},
    {name: "pre-opt deadcode", fn-3693: deadcode},
    ... ...
    {name: "regalloc", fn-3693: regalloc, required: true},   // allocate int & float registers + stack slots
    {name: "loop rotate", fn-3693: loopRotate},
    {name: "stackframe", fn-3693: stackframe, required: true},
    {name: "trim", fn-3693: trim}, // remove empty blocks
}

粗略数了一下,这里约有50个pass(其中包含多轮的deadcode清理),每个pass执行的代码都位于$GOROOT/src/cmd/compile/internal/ssa目录下,我们也可以通过dump出的html查看每一pass后得到的SSA结果,以ssa-sequential.html为例,其多轮优化的示意图如下:

点击浏览器页面上的黑体字优化标题(比如:lowered deadcode for cse),这一步产生的SSA代码都会显示出来,最后一个框框中是基于SSA生成目标架构的汇编代码。

每一个pass都有其独特性,比如cse,代表Common Subexpression Elimination(共同子表达式删除) ,下面是一个cse优化的例子:

y = x + 5
...
z = x + 5

cse优化后(前提中间过程中x值没变过):

y = x + 5
...
z = y

在这个示例中,经过一轮cse,Go便可以节省下一次没必要的加法运算(z = x + 5)。别看一次加法运算不起眼,积累多了也是不小的性能提升,

如果你对某一pass的优化动作感兴趣,可以对照$GOROOT/src/cmd/compile/internal/ssa目录下的代码与浏览器中生成的SSA来对其进行深入研究。

5. 小结

编译器后端的逻辑总是很难理解的,本文对Go编译器与SSA的渊源、Go编译器中驱动SSA转换和优化的环节以及Go生成的SSA的形式与过程做了介绍,算是对SSA入了个门。但要想真正搞懂SSA转换以及基于SSA的优化步骤的细节,认真阅读SSA相关的paper和资料(见参考资料)以及相关code是不可或缺的。

本文涉及的代码在这里可以下载。

6. 参考资料

  • 《编译原理(第二版)》- https://book.douban.com/subject/3296317/
  • SSA: Static Single-Assignment Form – https://www.slideserve.com/heidi-farmer/ssa-static-single-assignment-form
  • 《Static Single Assignment Book》 – https://pfalcon.github.io/ssabook/latest/book-full.pdf
  • Static single-assignment form – https://en.wikipedia.org/wiki/Static_single_assignment_form
  • GopherCon 2017: Keith Randall – Generating Better Machine Code with SSA – https://about.sourcegraph.com/blog/go/generating-better-machine-code-with-ssa
  • Generating Better Machine Code with SSA(slide) – https://raw.githubusercontent.com/gophercon/2017-talks/master/KeithRandall-GeneratingBetterMachineCodeWithSSA/GeneratingBetterMachineCodeWithSSA.pdf
  • New SSA Backend for the Go Compiler – https://docs.google.com/document/d/1szwabPJJc4J-igUZU4ZKprOrNRNJug2JPD8OYi3i1K0/edit

“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats