这是我参加「第五届青训营 」伴学笔记创作活动的第 8 天
前语
本文将针对Go
言语特性介绍Go
相关的功能优化主张、功能优化的原则和流程、常用的Go
言语程序优化手法。
简介
功能优化的前提是满足正确可靠、简洁明晰等质量要素;功能优化是综合评估,有时分时刻功率和空间功率或许对立。
Benchmark
功能表现需求实践数据衡量,Go
言语供给了支撑基准功能测验的工具——benchmark。
运用示例:
履行命令:go test -bench=. -benchmem
,输出成果如下:
goos: darwin
goarch: amd64
pkg: learning/fib
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.70GHz
BenchmarkFib-8 4045759 295.6 ns/op 0 B/op 0 allocs/op
PASS
ok learning/fib 2.102s
成果说明:
-
BenchmarkFib-8
为测验函数名,数字8
表明GOMAXPROCS
的值,代表CPU的核数。 -
4045759
表明总共履行了4045759
次。 -
295.6 ns/op
表明每次履行花费295.6
ns。 -
0 B/op
表明每次履行请求多大的内存。 -
0 allocs/op
表明每次履行请求了几回内存。
Slice
在运用slice
时,预分配内存,尽或许在运用make()
初始化切片时供给容量信息,削减后续内存分配次数。
对slice
预分配内存和不预分配内存的比照测验如下:
运转测验输出成果如下:
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.70GHz
BenchmarkNoPreAlloc-8 5835811 193.2 ns/op 248 B/op 5 allocs/op
BenchmarkPreAlloc-8 26007894 40.22 ns/op 80 B/op 1 allocs/op
PASS
ok command-line-arguments 2.849s
从输出的成果来看,分配内存的代码功率更高。为什么有这样的差异呢?
切片本质是一个数组片段的描绘包括:数组指针、片段的长度、片段的容量。切片操作并不是复制切片履行的元素,他会创立一个新的切片会复用本来切片的底层数组,会涉及到扩容操作,如果预分配好了数组长度,就会削减扩容的次数,从而进步功率。
slice圈套
slice
存在一个圈套:大内存未开释。
在已有切片基础上创立切片,不会创立新的底层数组。
场景:
- 原切片较大,代码在原切片基础上新建小切片
- 原底层数组在内存中有运用得不到开释
针对这种场景,能够运用copy
代替re-slice
。
Map
在运用map
时,也能够预分配内存,来提升功率。
下面经过基准测验来看下,运用预分配内存和不运用预分配内存的功能差异:
运转测验,输出成果如下:
BenchmarkNoPreAlloc-8 11716 108794 ns/op 86552 B/op 64 allocs/op
BenchmarkPreAlloc-8 29910 39307 ns/op 41097 B/op 6 allocs/op
呈现功能差异的原因是什么呢?由于在不断向map
中添加元素的操作会触发map
的扩容,而提早分配好空间能够削减内存复制和Rehash
的消耗。
所以在运用map
的时分,主张根据实践需求提早预估好需求的内存。
字符串处理
字符串拼接有许多种方法,常见的有直接加号拼接、strings.Builder
、bytes.Buffer
,下面经过基准测验比照下3
者的功能差异:
运转输出成果如下:
BenchmarkPlus-8 2569 471758 ns/op 1602938 B/op 999 allocs/op
BenchmarkStrBuilder-8 267066 4466 ns/op 8440 B/op 11 allocs/op
BenchmarkByteBuffer-8 122371 8971 ns/op 11200 B/op 8 allocs/op
能够看出,strings.Builder
的功能最好,bytes.Buffer
的功能次之,直接拼接功能最差。
原因分析:
- 字符串在Go言语中是不可变类型,占用内存大小是固定的
- 运用
+
拼接每次都会重新分配内存 -
strings.Builder
、bytes.Buffer
底层都是[]byte
数组 - 内存扩容策略,不需求每次拼接重新分配内存
预分配
结合slice
、map
,strings.Builder
、bytes.Buffer
也能够进行预分配,下面来测验下运用预分配和不运用预分配的功能差异:
运转测验,输出成果如下:
BenchmarkStrBuilder-8 286748 4252 ns/op 8440 B/op 11 allocs/op
BenchmarkByteBuffer-8 90979 11179 ns/op 11200 B/op 8 allocs/op
BenchmarkPreStrBuilder-8 183267 7690 ns/op 3072 B/op 1 allocs/op
BenchmarkPreByteBuffer-8 156591 7017 ns/op 6144 B/op 2 allocs/op
看一看到,运用预分配后,功能又会进一步的提升。
空结构体
功能优化有时是时刻和空间的平衡,之前说到的都是进步时刻功率的点,关于空间上是否有优化的手法呢? 空结构体是节约内存空间的一个手法。
运用空结构体节约内存,空结构体struct{}
实例不占有任何内存空间,可作为各种场景下的占位符运用,能够节约资源,空结构体自身具有很强的语义,即这儿不需求任何值,仅作为占位符。
下面来比照下,map
存储运用空结构体和运用bool
在内存空间上的差异。
运转测验,输出成果如下:
BenchmarkEmptyStructMap-8 15211 78213 ns/op 47734 B/op 65 allocs/op
BenchmarkBoolMap-8 14697 81723 ns/op 53308 B/op 72 allocs/op
从运转成果上来看,运用空结构体确实能节约一些空间。
空结构体struct{}
的一个典型的完成场景是完成Set
结构,Set
只需用到map
的键,不需求值,而空结构体不占用空间,即使是将map
的值设置为bool
类型,也会比空结构体多占有1个字节。
开源Set结构完成:github.com/deckarep/go…
atomic
在实践编程中经常会用到多线程编程,在Go
中有atomic
包,在这个包中会维护一个原子的变量,对值进行操作。或者经过加锁的方法操作。
下面经过示例来看下运用atomic
包和加锁的方法,比照一下两者的功能:
运转测验,输出成果如下:
BenchmarkAtomicAddOne-8 55781104 18.17 ns/op 4 B/op 1 allocs/op
BenchmarkMutexAddOne-8 31495927 36.35 ns/op 16 B/op 1 allocs/op
从测验成果能够看出,运用atomic
包的功能要比加锁的方法高。
原因分析:锁的完成是经过操作体系来完成的,属于体系调用;而atomic
操作是经过硬件完成,功率比锁高;sync.Mutex
应该用来维护一段逻辑,不仅仅用于维护一个变量;关于非数值操作,能够运用atomic.Value
,能承载一个interface{}
。
总结
关于功能优化,需求注意以下几点:
- 避免常见的功能圈套能够确保大部分程序的功能
- 一般使用代码,不要一味地寻求程序的功能
- 越高档的功能优化手法越容易呈现问题
- 在满足正确可靠、简洁明晰的质量要求的前提下进步程序功能
引用
功能优化攻略