这是我参与「第三届青训营 -后端场」笔记创造活动的第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中的-2GOPMAXPROCS,在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.Builderbytes.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/opallocs/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.memmove0.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.Builderbytes.Buffer等,当然它的本质必定程度也是在避免对内存的低效操作啦~

那这次的笔记共享就到这儿了,至于在对strings.Builder选用预分配战略遇到的“负优化”问题,还需求接下来继续探讨和研讨,也恳请大佬能给我指点迷津!

参阅链接

geektutu.com/post/hpg-be…