tech.bytedance.net/course-vide…
性能优化建议
-
简介
- 性能优化的前提是满足正确可靠、简洁清晰等质量因素
- 性能优化是综合评估,有时候时间效率和空间效率可能对立
- 针对Go语言特性,介绍Go相关的性能优化建议
1.3.1 Benchmark
- 性能表现需要实际数据衡量
- Go语言提供了支持基准性能测试的benchmark工具
go test -bench=. -benchmem
- GOMAXPROCS 1.5版本后,默认值为CPU核数
1.3.2 Slice性能优化建议
-
slice预分配内存
- 尽可能在使用make()初始化切片时提供容量信息
1.3.3 map性能优化建议
-
map预分配内存
-
分析
- 不断向map中添加元素的操作会触发map的扩容
- 提前分配好空间可以减少内存拷贝和Rehash的消耗
- 建议根据实际需求提前预估好需要的空间
-
-
测试代码
go
复制代码
func NoPreAlloc(size int) { data := make(map[int]int) for i := 0; i < size; i++ { data[i] = 1 } } func PreAlloc(size int) { data := make(map[int]int, size) for i := 0; i < size; i++ { data[i] = 1 } } func BenchmarkNoPreAlloc(b *testing.B) { for n := 0; n < b.N; n++ { NoPreAlloc(1000) } } func BenchmarkPreAlloc(b *testing.B) { for n := 0; n < b.N; n++ { PreAlloc(1000) } }
1.3.4 字符串处理
使用strings.Builder
-
使用+号拼接性能最差,strings.Builder,bytes.Buffer相近,strings.Builder更快
-
分析
- 字符串在Go语言中是不可变类型,占用内存大小是固定的
- 使用+每次都会重新分配内存
- strings.Builder,bytes.Buffer底层都是[]byte数组
- 内存扩容策略,不需要每次拼接重新分配内存
- bytes.Buffer转化为字符串时重新申请了一块空间
- strings.Builder直接将底层的[]byte转换成了字符串类型返回
- stirngs.Builder和bytes.Buffer都可以通过预分配内存来提高性能
测试代码
go
复制代码
func Plus(n int, str string) string { s := "" for i := 0; i < n; i++ { s += str } return s}func StrBuilder(n int, str string) string { var builder strings.Builder for i := 0; i < n; i++ { builder.WriteString(str) } return builder.String()}func ByteBuffer(n int, str string) string { buf := new(bytes.Buffer) for i := 0; i < n; i++ { buf.WriteString(str) } return buf.String()}func PreStrBuilder(n int, str string) string { var builder strings.Builder builder.Grow(n * len(str)) for i := 0; i < n; i++ { builder.WriteString(str) } return builder.String()}func PreByteBuffer(n int, str string) string { buf := new(bytes.Buffer) buf.Grow(n * len(str)) for i := 0; i < n; i++ { buf.WriteString(str) } return buf.String()}func BenchmarkPlus(b *testing.B) { for n := 0; n < b.N; n++ { Plus(1000, "string") }}func BenchmarkStrBuilder(b *testing.B) { for n := 0; n < b.N; n++ { StrBuilder(1000, "string") }}func BenchmarkByteBuffer(b *testing.B) { for n := 0; n < b.N; n++ { ByteBuffer(1000, "string") }}func BenchmarkPreStrBuilder(b *testing.B) { for n := 0; n < b.N; n++ { PreStrBuilder(1000, "string") }}func BenchmarkPreByteBuffer(b *testing.B) { for n := 0; n < b.N; n++ { PreByteBuffer(1000, "string") }}
1.3.5 空结构体
使用空结构体节省内存
-
空结构体struct{}实例不占据任何的内存空间
-
可作为各种场景下的占位符使用
- 节省资源
- 空结构体本身具备很强的语义,即这里不需要任何值,仅作为占位符
测试代码
go
复制代码
func EmptyStructMap(n int) { m := make(map[int]struct{}) for i := 0; i < n; i++ { m[i] = struct{}{} }}func BoolMap(n int) { m := make(map[int]bool) for i := 0; i < n; i++ { m[i] = false }}func BenchmarkEmptyStructMap(b *testing.B) { for n := 0; n < b.N; n++ { EmptyStructMap(10000) }}func BenchmarkBoolMap(b *testing.B) { for n := 0; n < b.N; n++ { BoolMap(10000) }}
-
实现Set,可以考虑用map来代替
- 对于这个场景,只需要用到map的键,而不需要值
- 即使是将map的值设置为bool类型,也会多占据1个字节空间
1.3.6 atomic包
使用atomic包
- 锁的实现是通过操作系统来实现,属于系统调用
- atomic操作是通过硬件实现,效率比锁高
- sync.Mutex应该用来保护一段逻辑,不仅仅用于保护一个变量
- 对于非数值操作,可以使用atomic.Value,能承载一个interface{}
测试代码
go
复制代码
type atomicCounter struct { i int32}func AtomicAddOne(c *atomicCounter) { atomic.AddInt32(&c.i, 1)}type mutexCounter struct { i int32 m sync.Mutex}func MutexAddOne(c *mutexCounter) { c.m.Lock() c.i++ c.m.Unlock()}func BenchmarkAtomicAddOne(b *testing.B) { for n := 0; n < b.N; n++ { var counter = atomicCounter{} AtomicAddOne(&counter) }}func BenchmarkMutexAddOne(b *testing.B) { for n := 0; n < b.N; n++ { var counter = mutexCounter{} MutexAddOne(&counter) }}
小结
- 避免常见的性能陷阱可以保证大部分程序的性能
- 普通应用代码,不要一味地追求程序地性能
- 越高级的性能优化手段越容易出现问题
- 在满足正确可靠,简洁清晰的质量要求的前提下提高程序性能