官方标准Go编译器中应该知道的一些优化
官方标准Go编译器
简称gc(Go compiler,非Garbage Collection)
另一款官方Go编译器为gccgo,主要做为一个参考编译器,帮助发现gc中的bugs和改善官方文档。目前从编译速度和编译出的代码质量(正确性和执行速度)都大大落后于gc。
Go设计中推荐使用编译器优化来弥补一些语言中缺失的小功能。
字符串和字节切片之间的转换
一般情况下,这样的转化需要复制一份底层的字节序列。但是gc编译器做了一些优化,使得下面这些情况下的这样的转化无需复制底层的字节序列:
紧跟range关键字的从字符串到字节切片的转换;
映射元素读取索引语法中被用做键值的从字节切片到字符串的转换;
字符串比较表达式中被用做比较值的从字节切片到字符串的转换;
(至少有一个被衔接的字符串值为非空字符串常量的)字符串衔接表达式中的从字节切片到字符串的转换
优化1:紧跟range关键字的从字符串到字节切片的转换
//BenchmarkRangeF
//BenchmarkRangeF-12 2228336 625 ns/op 0 B/op 0 allocs/op
//BenchmarkRangeG
//BenchmarkRangeG-12 1248417 973 ns/op 2048 B/op 1 allocs/op
var gogogo = strings.Repeat("Go", 1024)
func f1() {
for range []byte(gogogo) {
}
}
func g1() {
bs := []byte(gogogo)
for range bs {
}
}
func BenchmarkRangeF(b *testing.B) {
b.ReportAllocs()
for i:=0; i<b.N; i++ {
f1()
}
}
func BenchmarkRangeG(b *testing.B) {
b.ReportAllocs()
for i:=0; i<b.N; i++ {
g1()
}
}
优化2:映射元素读取索引语法中被用做键值的从字节切片到字符串的转换
var name = bytes.Repeat([]byte{'x'}, 33)
var m, s = make(map[string]string, 10), ""
//BenchmarkF2
//BenchmarkF2-12 424186015 2.99 ns/op 0 B/op 0 allocs/op
//BenchmarkG2
//BenchmarkG2-12 36853308 30.4 ns/op 48 B/op 1 allocs/op
//BenchmarkH2
//BenchmarkH2-12 24020469 46.7 ns/op 48 B/op 1 allocs/op
func f2() { s = m[string(name)] } // 有效
func g2() { key := string(name); s = m[key] } // 无效
func h2() { m[string(name)] = "Golang" } // 无效
func BenchmarkF2(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
f2()
}
}
func BenchmarkG2(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
g2()
}
}
func BenchmarkH2(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
h2()
}
}
优化3:一个字符串比较表达式中被用做比较值的字节切片到字符串的转换
//BenchmarkF3
//BenchmarkF3-12 61728664 18.9 ns/op 0 B/op 0 allocs/op
//BenchmarkG3
//BenchmarkG3-12 3114397 335 ns/op 2048 B/op 2 allocs/op
var x = []byte{1023: 'x'}
var y = []byte{1023: 'y'}
var b bool
func f3() { b = string(x) != string(y) }
func g3() { sx, sy := string(x), string(y); b = sx == sy }
func BenchmarkF3(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
f3()
}
}
func BenchmarkG3(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
g3()
}
}
优化4:含非空字符串常量的字符串衔接表达式中从字节切变到字符串的转换
//BenchmarkF4
//BenchmarkF4-12 3232948 334 ns/op 2304 B/op 1 allocs/op
//BenchmarkG4
//BenchmarkG4-12 1811911 610 ns/op 4096 B/op 3 allocs/op
var x4 = []byte{1023: 'x'}
var y4 = []byte{1023: 'y'}
func f4() { s = ("-" + string(x4) + string(y4))[1:] }
func g4() { s = string(x4) + string(y4) }
func BenchmarkF4(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
f4()
}
}
func BenchmarkG4(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
g4()
}
}
优化5:[]rune(aString)转换的时间和空间复杂度都是O(n),但是len([]rune(aString))中的此转换不需开辟内存
//BenchmarkF5
//BenchmarkF5-12 7204516 164 ns/op 0 B/op 0 allocs/op
//BenchmarkG5
//BenchmarkG5-12 18300470 61.4 ns/op 208 B/op 1 allocs/op
var GoGoGo = strings.Repeat("Go", 100)
func f5() { _ = len([]rune(GoGoGo)) }
func g5() { _ = len([]byte(GoGoGo)) } // 未对len([]byte(aString))做优化
func BenchmarkF5(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
f5()
}
}
func BenchmarkG5(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
g5()
}
}
优化6:字符串衔接表达式只需开辟一次内存,无论需要衔接多少个字符串
//BenchmarkF6
//BenchmarkF6-12 19159004 52.4 ns/op 32 B/op 1 allocs/op
//BenchmarkG6
//BenchmarkG6-12 9853606 117 ns/op 80 B/op 3 allocs/op
var x6, y6, z6, w = "Hello ", "World! ", "Let's ", "Go!"
var s6 string
func f6() { s6 = x6 + y6 + z6 + w }
func g6() { s6 = x6 + y6; s6 += z6; s6 += w }
func BenchmarkF6(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
f6()
}
}
func BenchmarkG6(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
g6()
}
}
优化7:for i := range anArrayOrSlice {anArrayOrSlice[i] = zeroElement}形式将被优化为一个内部的memclr操作
//BenchmarkClearArray
//BenchmarkClearArray-12 65026 17870 ns/op 0 B/op 0 allocs/op
//BenchmarkClearSlice
//BenchmarkClearSlice-12 65905 17934 ns/op 0 B/op 0 allocs/op
//BenchmarkClearArrayPtr
//BenchmarkClearArrayPtr-12 37152 31788 ns/op 0 B/op 0 allocs/op
const N = 1024 * 100
var a [N]int
func clearArray() {
for i := range a {
a[i] = 0
}
}
func clearSlice() {
s := a[:]
for i := range s {
s[i] = 0
}
}
func clearArrayPtr() {
for i := range &a {
a[i] = 0
}
} // 无效
func BenchmarkClearArray(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
clearArray()
}
}
func BenchmarkClearSlice(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
clearSlice()
}
}
func BenchmarkClearArrayPtr(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
clearArrayPtr()
}
}
优化8:for k = range m {delete(m, k)}形式将被优化为一个内部的map清空操作
这个优化貌似对于平时编程并没有太大的意义,因为这是清空map条目的唯一方法。但是其实有些Go程序员可能会通过用make来新开出来一个map的途径来变相清空map条目。
其实两种方法各有所长。目前官方标准编译器的实现中,一个map的底层哈希表数组的长度是永不收缩的。所以这个优化并不释放为底层哈希表数组开辟的内存。它只是比一个一个删除操作要快得多。
相比用make来新开出来一个map,此优化将减少一些GC(垃圾回收)压力。所以,具体应该使用哪种方法,视具体情况而定。
优化9:尺寸不大于4个原生字(即int)并且字段数不超过4个的结构体值被视为是小尺寸值
//Benchmark_Range1
//Benchmark_Range1-12 2205898 559 ns/op 0 B/op 0 allocs/op
//Benchmark_Range2
//Benchmark_Range2-12 3564176 330 ns/op 0 B/op 0 allocs/op
//Benchmark_Range3
//Benchmark_Range3-12 2275381 535 ns/op 0 B/op 0 allocs/op
//Benchmark_Range4
//Benchmark_Range4-12 3600985 338 ns/op 0 B/op 0 allocs/op
//Benchmark_Range5
//Benchmark_Range5-12 460306 2550 ns/op 0 B/op 0 allocs/op
//Benchmark_Range6
//Benchmark_Range6-12 412725 2792 ns/op 0 B/op 0 allocs/op
type S1 struct{ a int }
type S2 struct{ a, b int }
type S3 struct{ a, b, c int }
type S4 struct{ a, b, c, d int }
type S5 struct{ a, b, c, d, e int }
type S6 struct{ a, b, c, d, e, f int }
var ss1, ss2, ss3, ss4, ss5, ss6 = make([]S1, 1000), make([]S2, 1000),
make([]S3, 1000), make([]S4, 1000),
make([]S5, 1000), make([]S6, 1000)
var x1, x2, x3, x41, x5, x61 int
func Benchmark_Range1(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, v := range ss1 {
x1 = v.a
}
}
}
func Benchmark_Range2(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, v := range ss2 {
x2 = v.a
}
}
}
func Benchmark_Range3(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, v := range ss3 {
x3 = v.a
}
}
}
func Benchmark_Range4(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, v := range ss4 {
x41 = v.a
}
}
}
func Benchmark_Range5(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, v := range ss5 {
x5 = v.a
}
}
}
func Benchmark_Range6(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, v := range ss6 {
x61 = v.a
}
}
}
优化10:接口值包裹指针值比包裹其它类型的值要快,因为少开辟一次内存
//Benchmark_BoxPointer
//Benchmark_BoxPointer-12 1000000000 1.07 ns/op 0 B/op 0 allocs/op
//Benchmark_BoxInt
//Benchmark_BoxInt-12 90125618 13.1 ns/op 8 B/op 1 allocs/op
//Benchmark_BoxString
//Benchmark_BoxString-12 37528537 26.9 ns/op 16 B/op 1 allocs/op
//Benchmark_BoxSlice
//Benchmark_BoxSlice-12 29901456 34.3 ns/op 32 B/op 1 allocs/op
var p, xx, yy, z = new([]int), 256, "Go", []int{0}
var ip, ix, iy, iz interface{}
func Benchmark_BoxPointer(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
ip = p
} // pointer
}
func Benchmark_BoxInt(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
ix = xx
} // int
}
func Benchmark_BoxString(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
iy = yy
} // string
}
func Benchmark_BoxSlice(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
iz = z
} // slice
}
优化11:接口值包裹0-255的整数也很快(但是比直接包裹 指针还是慢一点)
//Benchmark_x
//Benchmark_x-12 420749977 2.44 ns/op 0 B/op 0 allocs/op
//Benchmark_y
//Benchmark_y-12 77580391 14.0 ns/op 8 B/op 1 allocs/op
var x_, y_ = 255, 256
var iy_, ix_ interface{}
// 从Go工具链1.15开始,个Go运行时内部维护着一个0到255的小数组。但包裹0-255的整数时,将直接包裹此数组的相应元素的指针而少开辟一次内存
// 测试环境Go version 1.15.1
func Benchmark_x(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
ix_ = x_
}
}
func Benchmark_y(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
iy_ = y_
}
}
BCE(Bounds Check Elimination)优化
Go是一门内存安全的语言。检查下标越界是保证内存安全的重要举措之一。但另一方面检查下标越界也耗费一些CPU计算。事实上绝大部分的下标越界检查都不会发现有问题的。这就是维护内存安全的代价。
在某些情形下,编译器在代码编译阶段可以确定某些下标越界检查是不必要的从而可以避免这些检查,这样将提升程序运行效率。
编译器并不总是足够得聪明,有时需要人为干预引导编译器来消除一些下标越界检查。
参考
最后更新于
这有帮助吗?