女主宣言

今天小编为大家分享一篇关于Go优化的文章,文章中会介绍一些技巧,通过这些技巧,我们可以事半功倍的提升程序性能。这些技巧只需要我们对程序稍加调整,不需要大的改动。希望能对大家有所帮助。

PS:丰富的一线技术、多元化的表现形式,尽在“360云计算”,点关注哦!

在本篇文章中,我们将会介绍一些技巧,通过这些技巧,我们可以事半功倍的提升程序性能。这些技巧只需要我们对程序稍加调整,那些需要对程序进行大量改动的优化,我们都会忽略掉,没有提及相应的技术。

0

开始之前

在对程序进行任何修改之前,我们需要花一些时间来做一个对比基准,以方便我们后面做比较。如果没有对比基准,我们的优化也变得没有意义,因为我们不确定到底是提升了还是降低了性能,是否有所改善。我们可以写一些基准测试,然后使用 pprof 进行一下测试。

1

使用sync.Pool复用已分配对象

sync.Pool 实现了一个 free list。这使我们可以重用先前分配的结构。这样可以缓冲对象的多次分配,减少内存回收的工作。这个API很简单,实现一个函数来分配新的对象实例,然后需要返回一个指针。

var bufpool = sync.Pool{New: func() interface{} {buf := make([]byte, 512)return &buf}
}

之后,我们可以使用 Get() 从池子中获取对象,Put() 返还使用后的资源。

// sync.Pool returns a interface{}: you must cast it to the underlying type
// before you use it.
bp := bufpool.Get().(*[]byte)
b := *bp
defer func() {*bp = bbufpool.Put(bp)
}()// Now, go do interesting things with your byte buffer.
buf := bytes.NewBuffer(b)

注意,在 Go1.13之前,每次进行垃圾收集时,都会清除池子,可能会影响程序的性能。

在把对象放回池子之前,必须要将数据结构中各字段数据进行清零。否则可能从池中获取到带有先前使用数据的脏对象。有可能带来严重安全风险。

type AuthenticationResponse {Token stringUserID string
}rsp := authPool.Get().(*AuthenticationResponse)
defer authPool.Put(rsp)// If we don't hit this if statement, we might return data from other users!
if blah {rsp.UserID = "user-1"rsp.Token = "super-secret
}return rsp

最安全的方式是,确保每次擦除内存可以这么操作:

// reset resets all fields of the AuthenticationResponse before pooling it.
func (a* AuthenticationResponse) reset() {a.Token = ""a.UserID = ""
}rsp := authPool.Get().(*AuthenticationResponse)
defer func() {rsp.reset()authPool.Put(rsp)
}()

唯一不会产生问题的情况是,你完全使用写入的内存。例如:

var (r io.Readerw io.Writer
)// Obtain a buffer from the pool.
buf := *bufPool.Get().(*[]byte)
defer bufPool.Put(&buf)// We only write to w exactly what we read from r, and no more.
nr, er := r.Read(buf)
if nr > 0 {nw, ew := w.Write(buf[0:nr])
}

2

避免使用指针为键的大map

在垃圾回收期间,运行时扫描包含指针的对象并对其进行追踪。如果有一个非常大的 map[string]int,则GC必须检查map中的每一个字符串,每次GC,因为字符串包含指针。

在示例中,我们将一千万个元素写入 map[sting]int,并进行来讲回收计时。我们在包范围内使用 map 确保是在堆内存中分配。

package mainimport ("fmt""runtime""strconv""time"
)const (numElements = 10000000
)var foo = map[string]int{}func timeGC() {t := time.Now()runtime.GC()fmt.Printf("gc took: %s\n", time.Since(t))
}func main() {for i := 0; i < numElements; i++ {foo[strconv.Itoa(i)] = i}for {timeGC()time.Sleep(1 * time.Second)}
}

执行程序,得到下面结果。

→ go install && inthash
gc took: 98.726321ms
gc took: 105.524633ms
gc took: 102.829451ms
gc took: 102.71908ms
gc took: 103.084104ms
gc took: 104.821989ms

我们怎么可以提升它呢?我们可以删除指针,将减少垃圾收集器需要跟踪的指针数量。字符串中包含指针,因此我们将其实现为 map[int]int。

package mainimport ("fmt""runtime""time"
)const (numElements = 10000000
)var foo = map[int]int{}func timeGC() {t := time.Now()runtime.GC()fmt.Printf("gc took: %s\n", time.Since(t))
}func main() {for i := 0; i < numElements; i++ {foo[i] = i}for {timeGC()time.Sleep(1 * time.Second)}
}

运行程序,看下面结果。

→ go install && inthash
gc took: 3.608993ms
gc took: 3.926913ms
gc took: 3.955706ms
gc took: 4.063795ms
gc took: 3.91519ms
gc took: 3.75226ms

看上去比较不错,已经将垃圾回收时间削减了97%。在生成用例中,插入之前可以将字符串进行hash,转为整数。

可以通过很多操作来逃避GC。如果要分配大量的无指针结构,整数或字节的巨型数组,GC将不会对其扫描,这意味着无需GC的开销。这种技术通常需要对程序进行实质性的改造,因此本篇文章中不对其深入研究。

3

生成编组代码避免运行时反射

将结构与JSON等各种序列化格式进行编组和解组是比较常见的一种操作。特别是在构建微服务时。实际上,你经常会发现大多数微服务实际上唯一要做的就是序列化。诸如json.Marshal 和 json.Unmarshal之类的函数依赖于运行时反射,将struct字段序列化为字节,反之亦然。这可能就会很慢,反射的性能远不如显式代码高。

但是,不必一定是这种方式。编组JSON的机制有点像这样:

package json// Marshal take an object and returns its representation in JSON.
func Marshal(obj interface{}) ([]byte, error) {// Check if this object knows how to marshal itself to JSON// by satisfying the Marshaller interface.if m, is := obj.(json.Marshaller); is {return m.MarshalJSON()}// It doesn't know how to marshal itself. Do default reflection based marshallling.return marshal(obj)
}

如果我们知道如何将我们的代码编组为JSON,可以使用钩子来避免运行时反射。但是又不想手写所有编组代码,那该怎么办?我们可以使用像easyjson这样的代码生成器查看结构,并生成高度优化的代码,这些代码与json.Marshaller等现有编组实现完全兼容。

下载包并执行,就可以生成我们的代码。$file.go 中包含我们的结构。

easyjson -all $file.go

会生成 $file_easyjson.go 文件,由于easyjson为我们实现了json.Marshaller接口,因此将调用这些函数,而不是基于反射的默认函数。

4

使用strings.Builder构建字符串

在Go中,字符串是不可变的,我们视为只读字符片段。这意味着每次创建字符串时,都将分配新的内存,并有可能为垃圾收集器创建更多工作。

Go1.10中,strings.Builder被引入为构建字符串的有效方法。在内部实现,它写入字节缓冲区。只有在生成器上调用String()时,才实际创建字符串。

进行性能比较:

// main.go
package mainimport "strings"var strs = []string{"here's","a","some","long","list","of","strings","for","you",
}func buildStrNaive() string {var s stringfor _, v := range strs {s += v}return s
}func buildStrBuilder() string {b := strings.Builder{}// Grow the buffer to a decent length, so we don't have to continually// re-allocate.b.Grow(60)for _, v := range strs {b.WriteString(v)}return b.String()
}
// main_test.go
package mainimport ("testing"
)var str stringfunc BenchmarkStringBuildNaive(b *testing.B) {for i := 0; i < b.N; i++ {str = buildStrNaive()}
}
func BenchmarkStringBuildBuilder(b *testing.B) {for i := 0; i < b.N; i++ {str = buildStrBuilder()}
}

得到下面结果:

→ go test -bench=. -benchmem
goos: darwin
goarch: amd64
pkg: github.com/sjwhitworth/perfblog/strbuild
BenchmarkStringBuildNaive-8          5000000           255 ns/op         216 B/op          8 allocs/op
BenchmarkStringBuildBuilder-8       20000000            54.9 ns/op        64 B/op          1 allocs/op

如上,strings.Builder的速度提高了4.7倍,分配次数的1/8,分配内存的1/4。

5

使用strconv代替fmt

fmt是Go中最常用的包之一。但是,当涉及到将整数转换为浮点数并转换为字符串时,它的性能不如其较低级的表兄弟 strconv。对于API的一些很小的更改,该包提供了很好的性能。

// main.go
package mainimport ("fmt""strconv"
)func strconvFmt(a string, b int) string {return a + ":" + strconv.Itoa(b)
}func fmtFmt(a string, b int) string {return fmt.Sprintf("%s:%d", a, b)
}func main() {}
// main_test.go
package mainimport ("testing"
)var (a    = "boo"blah = 42box  = ""
)func BenchmarkStrconv(b *testing.B) {for i := 0; i < b.N; i++ {box = strconvFmt(a, blah)}a = box
}func BenchmarkFmt(b *testing.B) {for i := 0; i < b.N; i++ {box = fmtFmt(a, blah)}a = box
}

测试结果:

→ go test -bench=. -benchmem
goos: darwin
goarch: amd64
pkg: github.com/sjwhitworth/perfblog/strfmt
BenchmarkStrconv-8      30000000            39.5 ns/op        32 B/op          1 allocs/op
BenchmarkFmt-8          10000000           143 ns/op          72 B/op          3 allocs/op

可以看到,strconv版本的速度快了3.5倍,分配次数的1/3,分配内存的一半。

6

在make中分配内存以避免重新分配

切片是Go中非常有用的构造。它提供了可调整大小的阵列,能够在不重新分配的情况下对同一基础内存采取不同的处理。如果深入看下切片的话,则切片由三个元素组成:

type slice struct {// pointer to underlying data in the slice.data uintptr// the number of elements in the slice.len int// the number of elements that the slice can// grow to before a new underlying array// is allocated.cap int
}

分别表示什么意思呢:

  • data:指向切片中基础数据的指针

  • len:切片中的当前元素数。

  • cap:重新分配之前切片可以增长到的元素数。

在内部,切片是定长数组。当达到切片的上限时,会分配一个新数组,其大小是前一个切片的上限的两倍,将内存从旧切片复制到新切片,并将旧数组丢弃

经常可以看到类似下面的代码,当预先知道分片的容量时,该分片会分配容量为零的分片。

var userIDs []string
for _, bar := range rsp.Users {userIDs = append(userIDs, bar.ID)
}

在这种情况下,切片长度和容量从零开始。收到请求后,我们将用户附加到切片。这样做时,我们达到了分片的容量:分配了一个新的基础数组,该数组是前一个分片的容量的两倍,并将分片中的数据复制到其中。如果我们在响应中有8个用户,这将导致5个分配。

一种更有效的方法是将其更改为以下格式:

userIDs := make([]string, 0, len(rsp.Users)for _, bar := range rsp.Users {userIDs = append(userIDs, bar.ID)
}

通过使用make,我们已将容量明确分配给切片。现在,我们可以追加到切片,我们不会触发其他分配和副本。

如果由于容量是动态的或在程序的稍后阶段计算出的容量而又不知道应该分配多少,可以测量程序运行时最终得到的切片大小的分布。我通常采用90%或99%的百分比,并在程序中对值进行硬编码。如果你需要在RAM和CPU之间进行权衡,请将此值设置为高于你所需的值。

7

使用可以传递字节片的方法

当使用包时,应该使用允许传递字节片的方法:这些方法通常可以更好地控制分配。

以 time.Format 与 time.AppendFormat 为例。time.Format 返回一个字符串。在底层分配了一个新的字节片并在其上调用 time.AppendFormat。time.AppendFormat 获取一个字节缓冲区,写入时间的格式表示,然后返回扩展的字节片。这在标准库的其他包中很常见。

为什么这可以提高性能?现在我们可以传递从 sync.Pool 获得的字节片,而不是每次都分配一个新的缓冲区。或者,我们可以将初始缓冲区大小增加到更适合程序的值,以减少切片重新复制。

总结

通过本篇文章,我们应该能够采用这些技术并将其应用于代码库中。时间长了之后,我们会构建一个心理模型来推理Go程序中的性能。这可以大大有助于前期设计。

360云计算

由360云平台团队打造的技术分享公众号,内容涉及数据库、大数据、微服务、容器、AIOps、IoT等众多技术领域,通过夯实的技术积累和丰富的一线实战经验,为你带来最有料的技术分享

简单几招优化你的Go程序相关推荐

  1. 从Java代码到Java堆理解和优化您的应用程序的内存使用

    从Java代码到Java堆理解和优化您的应用程序的内存使用 简介: 本文将为您提供 Java? 代码内存使用情况的深入见解,包括将 int 值置入一个Integer 对象的内存开销.对象委托的成本和不 ...

  2. 轻松使用计算机,职称计算机Excel辅导:简单四招让你使用Excel轻松提速

    1.闪电般地输入数据 当你可以使用填充柄一步就完成数据输入时,为什么还要浪费时间在Excel中输入一连串的数据呢?这个程序能够为你自动生成所有类型的数字和文本序列,它甚至还能够在你创建自定义序列的时候 ...

  3. 计算机如何永久删除文件无法找回,电脑文件永久性删除了怎么办?简单五招教你恢复...

    原标题:电脑文件永久性删除了怎么办?简单五招教你恢复 怎样算是将电脑中的文件彻底删除掉了?要说平常的文件删除工作,我们一般只会右键点击文件,然后选择删除,这样,在原本位置上就看不到该文件,但是说到底, ...

  4. 计算机桌面右键,电脑鼠标右键菜单太多了?简单几招帮你清理

    原标题:电脑鼠标右键菜单太多了?简单几招帮你清理 随着电脑中所安装的程序越来越多,右键菜单中的选项也都随着多了起来.这虽然说对我们使用电脑没有什么太大的影响,但是看着如此多的没用的右键菜单也是烦烦的, ...

  5. ppt中如何合并流程图_简单4招,教你轻松搞定PPT中的流程图!

    原标题:简单4招,教你轻松搞定PPT中的流程图! 来自:PK阿锴(ID:akaippt) 作者:王培锴 今天跟大家分享PPT流程图的制作方法,流程图相信大家都经常遇到,通常分布着许多节点,由线条链接起 ...

  6. 003.[python学习] 简单抓取豆瓣网电影信息程序

    003.[python学习] 简单抓取豆瓣网电影信息程序 声明:本程序仅用于学习爬网页数据,不可用于其它用途. 本程序仍有很多不足之处,请读者不吝赐教. 依赖:本程序依赖BeautifulSoup4和 ...

  7. 安卓手机反应慢又卡怎么办_手机变卡怎么办?简单一招叫教你解决!

    原标题:手机变卡怎么办?简单一招叫教你解决! 有很多人的手机永久了都会变得卡顿,有什么方法可以减少手机卡顿的问题呢,小编就来教你几招,让你可以在关键时刻轻松应对. 不常用的软件不仅会占用内存,还会拖慢 ...

  8. 华为 java sd卡_无需使用Micro SD卡!只需简单三招,轻松给华为手机扩展容量

    原标题:无需使用Micro SD卡!只需简单三招,轻松给华为手机扩展容量 这些年,智能手机不仅在摄像头像素上屡创新高,而且高端机型也普遍标配128GB存储,甚至出现512GB版本这样的巨无霸.总有一天 ...

  9. 安卓okhttp上传jason和图片_微信图片总是「已过期或被清理」?简单 3 招,可摆脱烦恼...

    微信图片总是「已过期或被清理」?简单 3 招可摆脱烦恼 除了文字.表格.PPT 和 PDF,大家日常办公中也常常和「图片文件」打交道. 于是,我们总碰到这样的问题: 1)图片文件太大,在线传输耗时,甲 ...

最新文章

  1. NDK学习笔记-使用现有so动态库
  2. Java--File文件操作
  3. 动态规划之神奇的口袋问题
  4. 真正的程序员(转贴)
  5. mac 中的 zip 和 unzip 命令
  6. 数据结构期末考题总结(附答案)
  7. 推导多元最小二乘法的计算方法
  8. iOS - 毛玻璃效果
  9. 你的独立思考能力正在被它吞噬掉
  10. 口罩机远程监控运维解决方案
  11. 家用计算机中的内存大约多少,64gb内存能装多少东西_64gb内存有多大-系统城
  12. 嵌入式开发专业术语概念汇总
  13. 从k8s集群主节点数量为什么是奇数来聊聊分布式系统
  14. oracle数据库 参考文献,数据库参考文献格式
  15. 根据数据库表字段动态生成选择画面[FREE_SELECTIONS_DIALOG]
  16. 计算机word宏,Word用宏来列出所有可用样式-word技巧-电脑技巧收藏家
  17. 【网络安全】SQL注入-XFF头注入
  18. 计算一元二次方程的根
  19. 为什么今天安全仍然重要
  20. 宏杰文件夹加密V2878【永久免费的文件加密软件】

热门文章

  1. Windows Phone 7 XNA触控操作之Gestures
  2. 安卓开发之如何利用Intent对象,实现Activity和另一个Activity之间的跳转
  3. YangTools从YANG生成Java类(Maven)
  4. php get获取cookie值,golang web开发获取get、post、cookie参数
  5. mesi协议怎么实现_volatile的底层实现原理
  6. python 打开excel并在屏幕上呈现_excel-检查文件是否在Python中打开
  7. java虚拟机_深入浅出Java虚拟机,从来没有人能把Java 虚拟机讲解的这么透彻
  8. 第19章,运维自动化之系统安装
  9. 1.10.返回四舍五入后的值.round()
  10. Jquery Mobile 画面导航栏共用的实现方法