为什么大神能够写出高性能代码?因为他们掌握了Go编译器所做的优化
以下文章来源于Go 101 ,作者老貘
下面将介绍Go标准编译器(截至Go SDK 1.12.x)所做的其它一些优化。
1. 多个字符串的衔接表达式在运行时只需开辟一次内存
一般来说,每两个字符串相衔接的时候,Go运行时开辟一段长度为这两个字符串的长度之和的内存用来存储结果字符串的字节序列。当衔接三个字符串的时候,将需要两次衔接操作,所以需要两次开辟内存。标准编译器做了一个优化,使得包含任意个字符串的字符串衔接表达式在执行时只需开辟一次内存。
比如,在下面这个程序中,函数g的效率要比函数f的效率高,因为函数g只需要开辟一次内存,而函数f却需开辟三次内存。
package main
import "fmt"
import "strings"
import "testing"
var x = strings.Repeat("x", 100)
var y = strings.Repeat("y", 100)
var z = strings.Repeat("z", 100)
var w = strings.Repeat("w", 100)
var sf, sg string
func f() {
sg = x + y
sg = sg + z
sg = sg + w
}
func g() {
sf = x + y + z + w
}
func main() {
// 3
fmt.Println(testing.AllocsPerRun(1, f))
// 1
fmt.Println(testing.AllocsPerRun(1, g))
}
2. 数组和切片元素的重置操作将被优化为一个内部的memclr函数调用
假设t0是一个类型T的零值的字面表示形式,并且a是一个元素类型为T的数组或者切片,则官方标准编译器将把下面的单循环变量for-range代码块优化为一个内部的memclr调用。大多数情况下,此memclr调用比一个一个地重置元素要快。
for i := range a {
a[i] = t0
}
3. 动态值为指针的接口值比动态值为非指针的接口值少开辟一块内存
在标准编译器的实现中,当一个非接口值被赋给一个接口值时,此非接口值将被复制一份,此副本地址做为一个指针字段将被存储在此接口值中。但是当此非接口值本身就是一个指针值时,此复制将被避免,此指针值将直接存储在此接口值中。这是标准编译器特别做出的一个优化。因此,
- 将一个指针值包裹到接口值中的操作比将一个非指针值包裹到接口值中要高效得多;
- 从一个接口值中类型断言出一个指针值的操作也比从一个接口值中类型断言出一个非指针值要高效得多。
4. 清除映射(map)值中的所有条目
形如下面这样的用来清除一个映射m中的所有条目的代码块将被特别优化,使得它比预期的(一个一个地移除条目的)执行效率要高得多。
for k := range m {
delete(m, k)
}
注意:目前Go标准编译器的实现中,为一个映射值开辟的内部底层哈希表数组的长度是永不缩减的。如果此映射值仍在被使用,则为它开辟的内存将肯定不会被回收,即使此映射从拥有大量的条目缩减为一个不含任何条目的映射。所以,如果需要在清除一个映射中的所有条目的同时并回收为此映射开辟的内存,请使用下面这种方法:
m = map[K]V{}
5. 形如append(a, make([]T, n)…)的切片扩张操作中的make函数调用不会开辟内存
一般可来说,每个make函数调用都需要开辟以此内存,但是形如append(a, make([]T, n)…)的代码中的make函数调用不会开辟内存。
此优化在Go SDK 1.11中被引入,但是目前(Go 1.12),此优化的效果还不如下面这样:
func SliceGrow(base []T, newCapacity int) []T {
s := make([]T, newCapacity)
copy(s, base)
return s
}
6. 统计一个字符串中的码点(rune)数操作len([]rune(aString))中的类型转换[]rune(aString)不会真正生成一个码点切片
一般来说,类型转换[]rune(aString)将生成一个类型为[]rune的码点切片,此过程需要为此码点切片的元素序列开辟一次内存。但是标准编译器对操作len([]rune(aString))做了特别的优化,使得此操作在执行时并没有生成[]rune码点切片,从而避免了一次内存开辟。
关于更多Go语言编程中的事实、细节和技巧,请访问《Go语言101》官方网站:https://gfw.go101.org 。