这是我参加「第五届青训营 」伴学笔记创作活动的第 8 天

前语

本文将针对Go言语特性介绍Go相关的功能优化主张、功能优化的原则和流程、常用的Go言语程序优化手法。

简介

功能优化的前提是满足正确可靠、简洁明晰等质量要素;功能优化是综合评估,有时分时刻功率和空间功率或许对立。

Benchmark

功能表现需求实践数据衡量,Go言语供给了支撑基准功能测验的工具——benchmark。

运用示例:

Go 性能优化——基准测试| 青训营笔记

履行命令: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预分配内存和不预分配内存的比照测验如下:

Go 性能优化——基准测试| 青训营笔记

运转测验输出成果如下:

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时,也能够预分配内存,来提升功率。

下面经过基准测验来看下,运用预分配内存和不运用预分配内存的功能差异:

Go 性能优化——基准测试| 青训营笔记

运转测验,输出成果如下:

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.Builderbytes.Buffer,下面经过基准测验比照下3者的功能差异:

Go 性能优化——基准测试| 青训营笔记

Go 性能优化——基准测试| 青训营笔记

运转输出成果如下:

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.Builderbytes.Buffer底层都是[]byte数组
  • 内存扩容策略,不需求每次拼接重新分配内存

预分配

结合slicemapstrings.Builderbytes.Buffer也能够进行预分配,下面来测验下运用预分配和不运用预分配的功能差异:

Go 性能优化——基准测试| 青训营笔记

运转测验,输出成果如下:

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在内存空间上的差异。

Go 性能优化——基准测试| 青训营笔记
运转测验,输出成果如下:

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包和加锁的方法,比照一下两者的功能:

Go 性能优化——基准测试| 青训营笔记

运转测验,输出成果如下:

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{}

总结

关于功能优化,需求注意以下几点:

  • 避免常见的功能圈套能够确保大部分程序的功能
  • 一般使用代码,不要一味地寻求程序的功能
  • 越高档的功能优化手法越容易呈现问题
  • 在满足正确可靠、简洁明晰的质量要求的前提下进步程序功能

引用

功能优化攻略