这篇博客是对 <Go 语言高性能编程>的读后总结, 原文中会有 demo 代码, 会有 benchmark 分析, 这里会尽可能的简短的去表达

字符串 string

临时拼接短的字符串, 可以用 + 的方式, 用 fmt.Sprintf() 都行, 但是因为字符串不可变,这些拼接都是生成一个新的字符串对象, 因此会占用 2N 体积的内存

针对大文本的字符串操作, 必须使用 strings.Builder, bytes.Buffer, []byte 这种切片拼接的方式

切片 slice

切片和数组类似, 区别就是会动态扩容, 1,2,4,8…..直到 1024 后按1.25的系数扩充

且切片拼接/向后追加是 O(N) 的时间复杂度, 随机插入/删除 的时间复杂度是 O(N), 增删比较频繁的还是建议使用链表

最大的大坑就是从同一个切片中出来的子切片是共享同一片内存的, 这就意味着:

1
2
3
arrA := []int{1,2,3,4,5}

arrB = arrA[0:1]

此时如果 arrB 一直在使用中, 或者发生内存逃逸, 那么是整块 arrA 的内存进行变动, 如果你不确定这块内存什么时候会被释放, 就使copy来处理切片

for 和 range 性能比较

由于 range 的特性, 它迭代的过程中是会进行<值拷贝>, 因此针对复杂结构的切片来说, for是更好的选择, 或者只用range迭代下标

1
2
3
4
5
6
7
8
m := map[string]int{
    "one":   1,
    "two":   2,
    "three": 3,
}
for k := range m {
    delete(m[k])
}

如果被迭代的对象是 int, byte, 指针 这种固定内存大小的数据, forrange其实相差不大

反射 relect

反射能让静态语言有一些动态的写法在里面, 但是非必要情况尽量少用反射,

一是因为逻辑不直观, 不利于后续维护;

二是性能略有损耗, 同时 reflect.New(typ).Elem().Field(0).SetString("name")reflect.New(typ).Elem().FieldByName("Name").SetString("name") 相比, 前者按下标来读取值位置, 要不根据 key 名读取性能高数倍, 因为 后者会遍历所有 key 名比对后找到对应下标再做更改

优化一下上述的问题,就使用临时缓存, 如示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func BenchmarkReflect_FieldByNameCacheSet(b *testing.B) {
	typ := reflect.TypeOf(Config{})
	cache := make(map[string]int)
	for i := 0; i < typ.NumField(); i++ {
		cache[typ.Field(i).Name] = i
	}
	ins := reflect.New(typ).Elem()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		ins.Field(cache["Name"]).SetString("name")
		ins.Field(cache["IP"]).SetString("ip")
		ins.Field(cache["URL"]).SetString("url")
		ins.Field(cache["Timeout"]).SetString("timeout")
	}
}

空结构 struct

空结构体的这个特性真的很不错, 因为struct{}是 不-占-内-存 的! 这个特性用来当占位符是再好不过了, 举个例子就是使用 map 来做集合:

1
2
3
4
5
mp := [string]struct{}{
    "a": struct{}{},
    "b": struct{}{},
    "c": struct{}{},
}

这个特性配合内存对齐来说就很好

内存对齐

cpu从内存中读取对象时并不是逐个访问字节, 而是按字长来一块一块的读取, golang的官方文档内存对齐规则是:

  • 对于任意类型的变量 x ,unsafe.Alignof(x) 至少为 1。
  • 对于 struct 结构体类型的变量 x,计算 x 每一个字段 f 的 unsafe.Alignof(x.f),unsafe.Alignof(x) 等于其中的最大值。
  • 对于 array 数组类型的变量 x,unsafe.Alignof(x) 等于构成数组的元素类型的对齐倍数。
  • 没有任何字段的空 struct{} 和没有任何元素的 array 占据的内存空间大小为 0,不同的大小为 0 的变量可能指向同一块地址。

举例来说:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
type demo1 struct {
	a int8
	b int16
	c int32
}

type demo2 struct {
	a int8
	c int32
	b int16
}

func main() {
	fmt.Println(unsafe.Sizeof(demo1{})) // 8
	fmt.Println(unsafe.Sizeof(demo2{})) // 12
}

一句话总结就是: 对象的类型定义要像金字塔一样, 字段类型的尺寸从上到下依次增大, 这样内存对齐时不会浪费空间(话说这个可以在编译阶段优化一下吧?)

这时的struct{} 这个类型也需要小小的注意一下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
type demo3 struct {
	c int32
	a struct{}
}

type demo4 struct {
	a struct{}
	c int32
}

func main() {
	fmt.Println(unsafe.Sizeof(demo3{})) // 8
	fmt.Println(unsafe.Sizeof(demo4{})) // 4
}