这是我参与「第三届青训营 -后端场」笔记创造活动的第1篇笔记
前言
功能优化能够说是软件开发中必不可少的一环,今日我想就课堂上的内容结合自己的考虑感悟就go言语程序的功能调优打开谈一谈。
go言语程序功能优化
简介
- 功能优化的前提是满意正确牢靠、简洁清晰等质量因素。
- 功能优化是综合评价与利害权衡,有时候时间功率与空间功率是敌对联系。
- 针对go言语特性,介绍go言语相关的功能优化办法。
基准测验之benchmark
go言语的规范库供给了相应的测验框架testing,其间也包含了基准测验benchmark的能力。
benchmark示例
以斐波那契数列为例,创立fib.go文件写入如下内容:
func Fib(n int) int {
if n < 2 {
return n
}
return Fib(n-1) + Fib(n-2)
}
再创立fib_test.go文件写入用于测验的代码,在go言语中,测验文件应以xxx_test.go方式命名,测验函数应以TestXxx/BenchmarkXxx的方式命名,Xxx为被测验函数名,fib_test.go文件内容如下:
import "testing"
func BenchmarkFib10(b *testing.B) {
//运转Fib函数b.N次
for i := 0; i < b.N; i++ {
Fib(10)
}
}
测验函数写好后,在测验文件目录下打开终端运转如下指令即可进行基准测验:
go test -bench=. -benchmem
-
-bench=.
表明在当前目录进行基准测验 -
-benchmem
表明统计内存信息
运转成果如下:
goos: linux
goarch: amd64
pkg: byteDance/5_8
cpu: Intel(R) Core(TM) i5-4210H CPU @ 2.90GHz
BenchmarkFib10-2 3275048 338.8 ns/op 0 B/op 0 allocs/op
PASS
ok byteDance/5_8 1.510s
-
BenchmarkFib10-2
中的-2
即GOPMAXPROCS
,在go1.5版别后默许等于cpu核数,可通过-cpu
参数进行更改,比方-cpu=1,2,3,4
-
3275048
表明总共履行的次数,即b.N的值 -
338.8 ns/op
表明每次履行耗时 -
0 B/op
表明每次履行请求的内存大小 -
0 allocs/op
表明每次履行分配内存的次数
功能优化之slice
运用slice时进行适当的预分配提高功能,代码比照方下:
func NoPreAlloc(size int) {
data := make([]int, 0)
for i := 0; i < size; i++ {
data = append(data, i)
}
}
func PreAlloc(size int) {
data := make([]int, 0, size)//预分配
for i := 0; i < size; i++ {
data = append(data, i)
}
}
-
PreAlloc
函数里对切片的cap进行了预分配,cap是切片隐含的一个特点,表明切片的最大容量
对两个函数选用相同的基准测验逻辑:
func BenchmarkNoPreAlloc(b *testing.B) {
for i := 0; i < b.N; i++ {
NoPreAlloc(100)
}
}
func BenchmarkPreAlloc(b *testing.B) {
for i := 0; i < b.N; i++ {
PreAlloc(100)
}
}
测验所用指令如下:
go test -bench="Alloc$" -benchmem
-
"-bench=Alloc$"
表明测验对象只包括以Alloc结尾的
基准测验成果如下:
BenchmarkNoPreAlloc-2 1872620 599.9 ns/op 1016 B/op 7 allocs/op
BenchmarkPreAlloc-2 6538225 180.7 ns/op 416 B/op 1 allocs/op
PASS
ok byteDance/5_8 3.954s
- 能够发现,两种战略每次履行所耗费的时间差距较大,选用预分配战略使得履行速度提高3倍多
- 预分配战略每次履行所请求的内存大小仅为416B,而没有预分配的战略每次履行请求的内存达到1016B
- 预分配战略每次履行只请求1次内存,而没有预分配的战略每次履行要请求7次内存之多
功能优化之map
同理,map也可选用类似的优化战略,测验代码与slice同理,不同的当地如下:
...
data := make(map[int]int)
...
...
data := make(map[int]int, size)
...
测验逻辑与slice选用相同的方式,不同的是将size大小作了调整,如下:
...
NoPreAlloc(30)
...
...
PreAlloc(30)
...
测验成果如下:
BenchmarkNoPreAlloc-2 340780 3303 ns/op 2218 B/op 6 allocs/op
BenchmarkPreAlloc-2 895104 1300 ns/op 1166 B/op 1 allocs/op
PASS
ok byteDance/5_8/map_test 3.618s
- 可见,预分配战略相同发挥了适当显著的效果
功能优化之string
string这种数据类型不同于slice和map,因此它的优化思路也略有不同。咱们无妨先认识一下go言语中string的底层完成。
string的底层完成
string类型的数据结构如下:
data | len |
---|---|
指向内存中字符串开端方位的指针 | 表明字符串的字节(非字符)个数 |
- golang将string类型分配到只读内存段,因此不能通过下标的方式对内容进行修正
- 多个string变量可共用一致字符串的某个部分,即多个string的
data
域指向同一块内存空间的某个方位 - 如需改动字符串的内容,需求开辟新的内存空间
优化思路
结合go言语string数据结构的特点,咱们意识到对string的优化离不开对内存操作的优化。
在日常的运用中,结合go言语的特点,人们一般对字符串的操作是直接用运算符来进行,比方拼接两个字符串选用+
,然而通过基准测验能够发现,这是一种十分低效的办法。那什么是更佳的办法呢?其实,在go言语规范库里的strings
包和bytes
包就供给了这样的办法——strings.Builder
和bytes.Buffer
口说无凭,详细写个测验来实践实践吧!代码如下:
func Plus(n int, str string) string {
s := ""
for i := 0; i < n; i++ {
s += str
}
return s
}
func StringsBuilder(n int, str string) string {
var builder strings.Builder
for i := 0; i < n; i++ {
builder.WriteString(str)
}
return builder.String()
}
func BytesBuffer(n int, str string) string {
var buffer bytes.Buffer
for i := 0; i < n; i++ {
buffer.WriteString(str)
}
return buffer.String()
}
测验逻辑如下:
func BenchmarkPlus(b *testing.B) {
for i := 0; i < b.N; i++ {
Plus(1000, "¥")
}
}
func BenchmarkStringsBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
StringsBuilder(1000, "¥")
}
}
func BenchmarkBytesBuffer(b *testing.B) {
for i := 0; i < b.N; i++ {
BytesBuffer(1000, "¥")
}
}
- 选用相同的测验逻辑,对字符
¥
累加1000次
测验成果如下:
BenchmarkPlus-2 1716 696485 ns/op 1602936 B/op 999 allocs/op
BenchmarkStringsBuilder-2 172130 6532 ns/op 8440 B/op 11 allocs/op
BenchmarkBytesBuffer-2 97269 11586 ns/op 12464 B/op 8 allocs/op
PASS
ok byteDance/5_8/string_test 7.158s
- 能够发现,选用
+
号拼接字符串的功率显着低于另两种办法,功率乃至慢了数十倍 - 而关于别的两种办法,它们也各有特点,其间
strings.Builder
办法每次操作会请求内存的次数会更多,而bytes.Buffer
办法每次操作请求的内存会更大,但从履行功率来讲,strings.Builder
会更胜一筹
另,通过将¥
修正为$
以后(其余参数不变),再进行测验会发现一些奇妙的变化,如下:
BenchmarkPlus-2 3513 326302 ns/op 530275 B/op 999 allocs/op
BenchmarkStringsBuilder-2 314575 4014 ns/op 3320 B/op 9 allocs/op
BenchmarkBytesBuffer-2 137330 8859 ns/op 3248 B/op 6 allocs/op
PASS
ok byteDance/5_8/string_test 5.968s
- 能够发现,
strings.Builder
办法这时除了allocs/op
更大以外,B/op
字段也更大了,这一点与之前的测验有所不同 - 不难想到,以上的奇妙变化与
¥
和$
字符在内存中占用不同的字节数有关
关于string优化的进一步探索
前面有谈到预分配的优化战略,那能不能用到string的优化当中呢?通过剖析发现,对string进行的拼接操作本质上也是对内存空间的操作,那运用上预分配战略是否能见效呢?咱们无妨试一试!
所改动部分的代码如下:
func PreAllocStringsBuilder(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 PreAllocBytesBuffer(n int, str string) string {
var buffer bytes.Buffer
//预分配
buffer.Grow(n * len(str))
for i := 0; i < n; i++ {
buffer.WriteString(str)
}
return buffer.String()
}
- 运用
Grow
办法对内存进行预分配
测验逻辑不变,仍然对$
字符串进行1000
次操作,得到的测验成果如下:
BenchmarkPlus-2 4584 295038 ns/op 530274 B/op 999 allocs/op
BenchmarkStringsBuilder-2 268663 4427 ns/op 3320 B/op 9 allocs/op
BenchmarkBytesBuffer-2 146083 8803 ns/op 3248 B/op 6 allocs/op
BenchmarkPreAllocStringsBuilder-2 202309 5794 ns/op 1024 B/op 1 allocs/op
BenchmarkPreAllocBytesBuffer-2 165926 7960 ns/op 2048 B/op 2 allocs/op
PASS
ok byteDance/5_8/string_test 10.494s
- 得到的测验成果令咱们既惊喜又惊讶,惊喜的是
bytes.Buffer
运用上预分配战略达到了预期的效果,而strings.Builder
运用上预分配战略却不如预期,反而在功率上让步了! - 能够发现,选用预分配战略后,
B/op
和allocs/op
都得到显着前进,那么致使strings.Builder
选用预分配战略后功能让步的原因是什么呢?因此我对这个问题进行了探索,见下方内容
StringsBuilder
“负优化”的探索与测验
运用pprof功能剖析东西对这两对测验逻辑进行剖析。
无预分配战略的成果如下:
File: string_test.test
Type: cpu
Time: May 30, 2022 at 2:13pm (CST)
Duration: 2.21s, Total samples = 2.19s (98.93%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 1710ms, 78.08% of 2190ms total
Dropped 34 nodes (cum <= 10.95ms)
Showing top 10 nodes out of 87
flat flat% sum% cum cum%
960ms 43.84% 43.84% 1710ms 78.08% strings.(*Builder).WriteString (inline)
160ms 7.31% 51.14% 1870ms 85.39% byteDance/5_8/string_test.StringsBuilder
140ms 6.39% 57.53% 150ms 6.85% strings.(*Builder).copyCheck
100ms 4.57% 62.10% 100ms 4.57% runtime.futex
80ms 3.65% 65.75% 590ms 26.94% runtime.growslice
70ms 3.20% 68.95% 370ms 16.89% runtime.mallocgc
70ms 3.20% 72.15% 70ms 3.20% runtime.memclrNoHeapPointers
50ms 2.28% 74.43% 70ms 3.20% runtime.scanobject
40ms 1.83% 76.26% 40ms 1.83% runtime.madvise
40ms 1.83% 78.08% 40ms 1.83% runtime.memmove
有预分配战略的成果如下:
File: string_test.test
Type: cpu
Time: May 30, 2022 at 2:13pm (CST)
Duration: 2.22s, Total samples = 2.14s (96.28%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 2.04s, 95.33% of 2.14s total
Dropped 36 nodes (cum <= 0.01s)
Showing top 10 nodes out of 37
flat flat% sum% cum cum%
0.83s 38.79% 38.79% 1.51s 70.56% strings.(*Builder).WriteString
0.52s 24.30% 63.08% 0.52s 24.30% runtime.memmove
0.40s 18.69% 81.78% 1.99s 92.99% byteDance/5_8/string_test.PreAllocStringsBuilder
0.11s 5.14% 86.92% 0.13s 6.07% strings.(*Builder).copyCheck (inline)
0.07s 3.27% 90.19% 0.07s 3.27% runtime.asyncPreempt
0.03s 1.40% 91.59% 0.03s 1.40% runtime.futex
0.02s 0.93% 92.52% 0.02s 0.93% runtime.memclrNoHeapPointers
0.02s 0.93% 93.46% 0.02s 0.93% runtime.nanotime
0.02s 0.93% 94.39% 0.02s 0.93% runtime.nanotime1
0.02s 0.93% 95.33% 0.02s 0.93% runtime.osyield
- 能够发现,问题出在
0.52s 24.30% 63.08% 0.52s 24.30% runtime.memmove
和0.40s 18.69% 81.78% 1.99s 92.99% byteDance/5_8/string_test.PreAllocStringsBuilder
这两行
在pprof中运转list
指令,继续比照剖析StringsBuilder
函数和PreAllocStringsBuilder
函数,成果如下:
(pprof) list StringsBuilder
...
. . 16:func StringsBuilder(n int, str string) string {
. . 17: var builder strings.Builder
150ms 150ms 18: for i := 0; i < n; i++ {
10ms 1.72s 19: builder.WriteString(str)
. . 20: }
. . 21: return builder.String()
. . 22:}
...
(pprof) list PreAllocStringsBuilder
...
. . 32:func PreAllocStringsBuilder(n int, str string) string {
10ms 10ms 33: var builder strings.Builder
20ms 80ms 34: builder.Grow(n * len(str))
330ms 350ms 35: for i := 0; i < n; i++ {
40ms 1.55s 36: builder.WriteString(str)
. . 37: }
. . 38: return builder.String()
. . 39:}
...
- 比照发现,
builder.WriteString
的耗时满意预期,可是关于相同一段代码for i := 0; i < n; i++ {
,且传入相同的参数,为什么PreAllocStringsBuilder
中的耗时多出那么多,让我摸不着头脑T_T
另,关于PreAllocStringsBuilder
中的runtime.memmove
为什么比StringsBuilder
中的多出那么多,暂时也没有条理,不太能理解选用预分配战略为什么会导致runtime.memmove
更多了…T_T,希望能有大佬帮我回答疑惑吧!
总结
在前述内容里,咱们探讨了go言语程序功能优化的一个思路——预分配战略,由于程序的运转离不开对内存的操作,如何更好更高效地操作内存必然会必定程度得提高程序运转的功率。
别的关于像string这样的数据类型,也有特殊的优化思路,针关于字符串的拼接操作,能够运用更高效的库函数或许东西如strings.Builder
和bytes.Buffer
等,当然它的本质必定程度也是在避免对内存的低效操作啦~
那这次的笔记共享就到这儿了,至于在对strings.Builder
选用预分配战略遇到的“负优化”问题,还需求接下来继续探讨和研讨,也恳请大佬能给我指点迷津!
参阅链接
geektutu.com/post/hpg-be…