如何快速提升 Go 程序性能?_丽柜官网

on

摘要:

    小规模时性能还不错以外丽贵官网,但用户数增长后就无法使用的操作。这类操作通常是O(N)或O(N²)。当用户基数小时,它们的性能还可以接受,所以经常在将产品推向市场阶段使用。随着用户基数的增长,这类操作会出现越来越多意想不到的异常,服务也变得不稳定。出于不同人之手的大量小型

快速提升 Go 程序根本丝袜美足性能的实用技巧,你值得了解一下。

其次丽柜官网

译者 | 弯月拿美腿写真,责编 | 屠敏

出品 | CSDN(ID:CSDNnews)

我对软件性能的话题十分感兴趣。虽然我说不清究竟是为什么。我忍受不了慢吞吞的服务和程序,而且似乎有此种感受的人不止我一个,比如还有Greg Linden:

我们尝试过在A/B测试中,将页面的延迟增加100毫秒,结果发现如此微小的延迟也会导致整体如前所述丽柜官网性能的大幅下降。

——亚马逊,Greg Linden仅丝袜美足

根据我的经验,糟糕的性能通常来自两个那会儿北京丽柜方面:

小规模时性能还不错,但用户数增长后就无法使用的操作。这类操作通常是O(N)或O(N²)。当用户基数小时,它们的性能还可以接受,所以经常在将产品很丽柜美束推向市场阶段使用。随着用户基数的增长,这类操作会出现越来越多意想不到的异常,服务也变得不稳定。

出于不同人之手的大量小型优化——即“千疮百孔”的复杂代码。

我的职业生涯主要做两项哩ligui官网工作,第一是用Python写数据科学脚本,第二是用Go写服务。关于后者我有更多的优化经验。Go通常不会是服务的瓶颈,因为这些程序通常需要访问数据库,因此更偏重于IO。相反,批处理的机器学习管线(前者)的程序通常偏重于CPU。如果Go语言使用了过多CPU,而且造成了负面影响,那么你可以采用几种策略来应对。

我在这篇文章中列出了一些不需要太多精力就能显著提高性能的技巧,并不包含那些需要太多精力或需要大幅度修改程序结构的技巧。

开始优化之前

开始优化之前,首先应该花些时间找出一个合适的基准线,以便稍后比较。如果没有基准,那就等于摸着石头过河,根本不知道自己的优化有没有效果。首先要编写性能测试程序,然后生成能用于pprof的profile文件。最好可以编写Go的性能测试脚本(https://dave.cheney.net/2013/06/30/how-to-write-benchmarks-in-go),这样可以很容易地使用pprof,还可以评测内存分配情况。还可以使用benchcmp,这个工具可以帮助比较两次性能测试之间的性能差异。

如果代码很难做性能测试,那就从你能测量时间的部分开始。可以利用runtime/pprof手工测量代码。

现在开始吧!

使用sync.Pool重用之前分配过的对象

sync.Pool实现了一个空闲列表(free-list)。这样可以重新使用之前分配过的对象。这样做可以将对象分配的代价平摊到多次使用上,减少垃圾回收器的工作。API非常简单:只需实现一个函数,用来分配新的对象即可。它会返回指针类型。

var bufpool = sync.Pool{

New: func一片北京丽柜 interface{} {

buf := make(byte, 512)

return &buf

姑且ligui官网}}

之后,可以用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咱美腿写真 = b

bufpool.Put(bp)

日益丽贵官网}

// Now, go do interesting things with your byte buffer.

buf := bytes.NewBuffer(b)

不过要注意一些陷阱。在Go 1.13之前,每次发生垃圾回收时该池都会被清空。对于需要分配大量对象的程序来说,这可能会造成性能的影响。在1.13版本中似乎GC后能保留更多对象了(https://go-review当中丝袜美足.googlesource.com/c/go/+/162919/)。

你可能需要在将对象放回池中之前将其结构的字段清空。

如果不这样做,就可能从池中获得一个“脏”的对象,它包含之前使用过的数据发现北京丽柜。这可能会造成严重的安全问题!

type啊哟ligui官网 AuthenticationResponse {

Token充其量丽柜美束 string

UserID随着ligui官网 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)

不会ligui官网}

唯一不会发生问题的情况就是读取和写入时使用的内存是同一片的情况。例如:

var若是丝袜美足 (

r io由于丽柜官网.Reader

w io其他美腿写真.Writer

过来北京丽柜)

// Obtain a buffer from the pool你的ligui官网.

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])

哪样丽柜美束}

在大map中避免使用包含指针的结构作为map的键

关于Go中大型堆的性能问题已经有很多人讨论过了。在垃圾回收过程中,运行时会扫描包含指针的对象并遍历其指针。如果你有非常大的map[string]int望丽柜官网,那么垃圾回收器就不得不在每次垃圾回收过程中检查map中的每个字符串,因为字符串包含指针。

这个例子中我们向一个map[string]int最后丽柜官网中写入了一千万个元素,然后测量垃圾回收的时间。map是在包的作用域中分配的,以保证它被分配到堆上。

package main从事ligui官网

import (

“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)

开展丽贵官网}

显著丝袜美足}

运行所谓美腿写真后可以得到以下结果:

🍎 inthash → 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 main

import (

“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

明确ligui官网}

谁丽柜官网for {

timeGC

time.Sleep恰巧ligui官网(1 * time.Second)

不断ligui官网}

主义丝袜美足}

重新运行程序,结果如下:

🍎 inthash → go install && inthash

gc took: 3.608993ms

gc缕缕ligui官网 took: 3.926913ms

gc took: 3三番两次美腿写真.955706ms

gc took: 4并没丽柜官网.063795ms

gc took: 3自身丽贵官网.91519ms

gc took: 3.75226ms来讲丽柜视频

好多了。垃圾回收的时间减少了97%。在生产环境下,字符串需要进行hash之后再插入到map中。

还有许多技巧可以避免垃圾回收。如果为一个不含指针的结构(如int或byte)分配了巨大的数组,那么垃圾回收器就不会扫描它,意味着没有任何垃圾回收的额外开销。这种技巧通常需要重写大量程序,所以这里就不再细谈了。

与任何优化一样,该技巧的效果因人而异。Damian Gryski的一系列推特(https://twitter.com/dgryski/status/1140685755578118144)介绍了一个有趣的例子,从一个大型map中去掉字符串而使用智能数据结构,实际上会增加内存开销。我建议你阅读下他的文章。

生成marshalling和丽柜视频代码以避免运行时反射

将数据结构marshalh或unmarshal成JSON等各种序列化格式是个很常见的操作,特别是在构建微服务的时候。实际上,大部分微服务做的唯一工作就是序列化。像json.Marshal和json.Unmarshal需要依赖运行时反射才能将结构体的字段序列化成字节,反之亦然。这个操作很慢,反射的性能完全无法与显式的代码相比。

但我们不必这么做。marshalling 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)

今天北京丽柜}

如果我们知道怎样将对象marshal成JSON,就应该避免运行时反射。但我们不想手工marshal所有代码,怎么办呢?可以让计算机替我们写程序!像easyjson等代码生成器会检查接下来北京丽柜结构体,然后生成高度优化且与json.Marshaller等接口完全兼容的代码。

下载这个包,然后在包含结构体的$file.go上运行下面的命令:

easyjson -all $file有力丽柜视频.go

这个命令会生成$file_easyjson.go。由于easyjson为我们实现了json.Marshaller接口,因此序列化时不会调用默认的反射,而是会使用生成的函数。祝贺你!你已经将JSON marshalling的代码的速度提高便于丽柜官网了三倍。还有许多其他技巧可以进一步提升性能

我推荐这个包,是因为我之前用过,而且效果非常不错。不过请不要以此为契机跟我争论哪个JSON marshal的包最快。

需要确保在结构体改变后重新生成marshalling的代码。如果忘记,那么新的字段就不会被序列化和反序列化,会给编程造成困扰!可以调用go generate来处理代码生成。为了保证代码与结构体同步,我会在包的根目录下放一个generate.go文件,它会针对保重的所有文件调用go generate,这样可以帮助我处理众多需要生成的文件。小提示:在CI过程中调用go generate,检查生成的代码与已提交的代码是否有区别,来确保结构体是最新的。

使用strings.Builder来构建字符串

Go语言的字符串是不可修改的,可以认为它们是只读的字节切片。这就是说,每次创建字符串都要分配新的内存按时美腿写真,可能还会给垃圾回收器造成更多工作。

Go 1.10引入了strings.Builder作为高效率构建字符串的方式。它内部会将字符串写入到字节具体说来丽柜官网缓冲区。只有在builder上调用String时才会真正生成字符串。它依赖一些unsafe的技巧将底层的字节作为字符串返回,而不实际进行内存非配。这篇文章(https://syslog.ravelin.com/byte-vs-string-in-go-d645b67ca7ff)介绍了更多其工作原理。

我们来比较下两种方式的性能:

// main倘然丽柜官网.go

package main

import嗡北京丽柜 “strings”

var作用美腿写真 strs = string{

瑟瑟北京丽柜”here’s”,

不妨丽柜官网“a”,

“some”,

“long”,

“list”,

赖以美腿写真”of”,

“strings”,

嘿嘿北京丽柜”for”,

除ligui官网“you”,

论说丽柜美束}

func buildStrNaive几番丽柜美束 string {

var若是北京丽柜 s string

for _, 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咱丝袜美足)

b.WriteString(v)

从宽丽柜官网}

return b.String

结构北京丽柜}

// main_test.go

package今丽柜官网 main

import二来北京丽柜 (

“testing”

趁ligui官网)

var str加之ligui官网 string

func BenchmarkStringBuildNaive(b *testing当场丽贵官网.B) {

for i := 0嘎丽柜官网; i < b.N; i++ {

str = buildStrNaive

而又美腿写真}

上升丽贵官网}

func简而言之丽柜官网 BenchmarkStringBuildBuilder(b *testing.B) {

str = buildStrBuilder行动丽柜官网

半ligui官网}

在我的Macbook Pro上的结果如下:

🍎 strbuild → 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。

所以,在性能重要的时候应该使用strings.Builder。一般来说,除非是非常不重要的情况,否则我建议永远使用strings.Builder来构建字符串。

使用strconv代替fmt

fmt是Go中最著名的包之一。估计你从第一个Go程序——输出“hello, world”——的时候就开始用它了。但是,如果需要将整数和浮点数转换成字符串,它就不如更底层的strconv有效率了。strconv只需要在API中进行很小改动,就能带来不错的性能提升。

大多数情况下fmt会接受一个interface{}作为参数。这样做有两个必定丽柜官网弊端:

失去了类型安全。对于我来说这是个很大的问题。

会增加内存分配次数。将非指针类型作为interface{}传递通常会导致堆分配的问题。进一步的内容可以阅读这篇文章(https小丝袜美足://www.darkcoding.net/software/go-the-price-of-interface/)。

下面的程序显示了性能上的差异眨眼丽柜美束:

// main.go

package main即将丝袜美足

import (

“fmt让丽柜视频”

“strconv”

论丝袜美足)

func strconvFmt(a string, b int) string {

return让丽柜官网 a + “:” + strconv.Itoa(b)

屡ligui官网}

func宁愿丽柜美束 fmtFmt(a string, b int) string {

return fmt对ligui官网.Sprintf(“%s:%d”, a, b)

一起丽贵官网}

func main {}

// main_test.go

package main

import嘎丽贵官网 (

“testing”

定丽贵官网)

var不少丽柜美束 (

a = “boo”

blah = 42

box = “”

多少丝袜美足)

func BenchmarkStrconv(b *testing.B) {

for i := 0竟然美腿写真; i < b.N; i++ {

box = strconvFmt既是ligui官网(a, blah)

人丝袜美足}

a = box左右丝袜美足

据此丽贵官网}

func BenchmarkFmt(b *testing.B) {

box = fmtFmt(a, blah)

互丽柜视频}

a = box加强丽柜官网

而况丽柜官网}

在Macbook Pro上的测试结果:

🍎 strfmt → 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,内存分配量是1/2。

在make中指定分配的容量来避免重新分配

在讨论性能改善之前,我们先来迅速看一下切片。切片是Go语言中一个非常有用的概念。它提供了可改变大小的数组,还可以用不同的方式表示同一片底层内存区域,而不需要重新进行内存分配。slice的内部结构由三个元素组成:

type管ligui官网 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:在不重新分配内存的前提下,切片能够增长到企图丽柜视频的元素数目

在底层,切片是固定长度的数组。当长度增长到cap极丽柜官网时,就会重新分配一个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显式声明切片的容量。接下来可以向切片添加元素,而不会触发内存重新分配和拷贝说明ligui官网

如果数量是动态的,或只能稍后计算,从而无法预先得知应该分配多少内存,可以先运行程序,然后测量一下内存大小的实际分布情况。我一般会取90或99百分位数,将这个数字写到程序中。如果你愿意用RAM换取CPU时间,可以设置更高的值。

这条建议也适用于map:适用make(map[string]string, len(foo))可以分配足够的底层内存以避免内存重新分配打丝袜美足。

关于切片的工作原理可以参见这篇文章“Go切片:用法和内部原理”(https://blog从头ligui官网.golang.org/go-slices-usage-and-internals)。

使用可以接受继后北京丽柜字节切片的方法

在使用包时,寻找那些接受字节切片作为参数的方法,这些方法通常给你更多控制内存分配种丽柜官网的自由。

一个很好的例子就是time.Format和time.AppendFormat。time.Format返回字符串。内部会分配一个新的字节切片,然后在其上调用time.AppendFormat。而time.AppendFormat接受一个字节缓冲区,将格式化后的时间写入缓冲区,然后返回扩展后的字节切片。标准库中这种做法非常常见,如strconv.AppendFloat或bytes.NewBuffer。

为什么这样能提高鄙人ligui官网性能?因为你可以传递从sync.Poolh获得的字节切片,而不需要每次都分配新的缓冲区。或者可以初始化一个足够大的缓冲区,来减少切片拷贝。

故而丝袜美足总结

读完这篇文章后,你应该可以在代码中应用这些技巧了。长期坚持这种做法,你就会习惯于考虑Go程序的性能。这会极大地影响你的设计能力。

作为结语,我需要提醒一点。我的这些建议多年前美腿写真只是某些具体情况下的建议,而不是真理。一定要自己测量性能

要知道何时该停止优化。提高系统性能会让工程师感觉非常满足:问题本身很有趣,也有立竿见影的效果。但是,提高性能带来的效果非常依赖于具体情况。如果服务的响应时间只有10毫秒,而网络访问需要90毫秒,那么将10毫秒优化到5毫秒就完全不值得,因为你依然需要95毫秒。就算你将响应时间优化到1毫秒,最后结果还是91毫秒。你应该去做其他更有价值的事情。

用心奋勇丝袜美足优化!

原文:https://stephen.sh敢情北京丽柜/posts/quick-go-performance-improvements

作者:Stephen Whitworth每年丽贵官网,软件工程师@monzo,https://www.ravelin.com/的合伙创始人。

本文为 CSDN 翻译,转载请注明来源出处。


Warning: count(): Parameter must be an array or an object that implements Countable in /home/wdir/ligui88/wp-includes/class-wp-comment-query.php on line 399

发表评论

邮箱地址不会被公开。 必填项已用*标注