Golang性能调优示例

1. 性能调优过程

性能调试总体思路

image-20200802051340009

2. 常见的分析指标

1. Wall Time

即墙上时钟时间(wall clock time):从进程从开始运行到结束,时钟走过的时间,这其中包含了进程在阻塞和等待状态的时间。

2. CPU Time

1. 用户CPU时间

就是用户的进程获得了CPU资源以后,在用户态执行的时间

2. 系统CPU时间

用户进程获得了CPU资源以后,在内核态的执行时间。

3. Block Time

4. Memery allocation

5. GC times/time spent

3. 调优示例

1. 源代码

optmization.go

package profiling

import (
	"encoding/json"
	"strconv"
	"strings"
)

func createRequest() string {
	payload := make([]int, 100, 100)
	for i := 0; i < 100; i++ {
		payload[i] = i
	}
	req := Request{"demo_transaction", payload}
	v, err := json.Marshal(&req)
	if err != nil {
		panic(err)
	}
	return string(v)
}

func processRequest(reqs []string) []string {
	reps := []string{}
	for _, req := range reqs {
		reqObj := &Request{}
		reqObj.UnmarshalJSON([]byte(req))
		//	json.Unmarshal([]byte(req), reqObj)

		var buf strings.Builder
		for _, e := range reqObj.PayLoad {
			buf.WriteString(strconv.Itoa(e))
			buf.WriteString(",")
		}
		repObj := &Response{reqObj.TransactionID, buf.String()}
		repJson, err := repObj.MarshalJSON()
		//repJson, err := json.Marshal(&repObj)
		if err != nil {
			panic(err)
		}
		reps = append(reps, string(repJson))
	}
	return reps
}

structs.go

package profiling

type Request struct {
	TransactionID string `json:"transaction_id"`
	PayLoad       []int  `json:"payload"`
}

type Response struct {
	TransactionID string `json:"transaction_id"`
	Expression    string `json:"exp"`
}

structs_easyjson.go

这段代码是自动生成的,不是手写的,用的是easyjson这个库,这个库的作用是反序列化json,比go lib中原始的json.Unmarshal要快很多。

// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT.

package profiling

import (
	json "encoding/json"
	easyjson "github.com/mailru/easyjson"
	jlexer "github.com/mailru/easyjson/jlexer"
	jwriter "github.com/mailru/easyjson/jwriter"
)

// suppress unused package warning
var (
	_ *json.RawMessage
	_ *jlexer.Lexer
	_ *jwriter.Writer
	_ easyjson.Marshaler
)

func easyjson6a975c40DecodeCh47(in *jlexer.Lexer, out *Response) {
	isTopLevel := in.IsStart()
	if in.IsNull() {
		if isTopLevel {
			in.Consumed()
		}
		in.Skip()
		return
	}
	in.Delim('{')
	for !in.IsDelim('}') {
		key := in.UnsafeString()
		in.WantColon()
		if in.IsNull() {
			in.Skip()
			in.WantComma()
			continue
		}
		switch key {
		case "transaction_id":
			out.TransactionID = string(in.String())
		case "exp":
			out.Expression = string(in.String())
		default:
			in.SkipRecursive()
		}
		in.WantComma()
	}
	in.Delim('}')
	if isTopLevel {
		in.Consumed()
	}
}
func easyjson6a975c40EncodeCh47(out *jwriter.Writer, in Response) {
	out.RawByte('{')
	first := true
	_ = first
	{
		const prefix string = ",\"transaction_id\":"
		if first {
			first = false
			out.RawString(prefix[1:])
		} else {
			out.RawString(prefix)
		}
		out.String(string(in.TransactionID))
	}
	{
		const prefix string = ",\"exp\":"
		if first {
			first = false
			out.RawString(prefix[1:])
		} else {
			out.RawString(prefix)
		}
		out.String(string(in.Expression))
	}
	out.RawByte('}')
}

// MarshalJSON supports json.Marshaler interface
func (v Response) MarshalJSON() ([]byte, error) {
	w := jwriter.Writer{}
	easyjson6a975c40EncodeCh47(&w, v)
	return w.Buffer.BuildBytes(), w.Error
}

// MarshalEasyJSON supports easyjson.Marshaler interface
func (v Response) MarshalEasyJSON(w *jwriter.Writer) {
	easyjson6a975c40EncodeCh47(w, v)
}

// UnmarshalJSON supports json.Unmarshaler interface
func (v *Response) UnmarshalJSON(data []byte) error {
	r := jlexer.Lexer{Data: data}
	easyjson6a975c40DecodeCh47(&r, v)
	return r.Error()
}

// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
func (v *Response) UnmarshalEasyJSON(l *jlexer.Lexer) {
	easyjson6a975c40DecodeCh47(l, v)
}
func easyjson6a975c40DecodeCh471(in *jlexer.Lexer, out *Request) {
	isTopLevel := in.IsStart()
	if in.IsNull() {
		if isTopLevel {
			in.Consumed()
		}
		in.Skip()
		return
	}
	in.Delim('{')
	for !in.IsDelim('}') {
		key := in.UnsafeString()
		in.WantColon()
		if in.IsNull() {
			in.Skip()
			in.WantComma()
			continue
		}
		switch key {
		case "transaction_id":
			out.TransactionID = string(in.String())
		case "payload":
			if in.IsNull() {
				in.Skip()
				out.PayLoad = nil
			} else {
				in.Delim('[')
				if out.PayLoad == nil {
					if !in.IsDelim(']') {
						out.PayLoad = make([]int, 0, 8)
					} else {
						out.PayLoad = []int{}
					}
				} else {
					out.PayLoad = (out.PayLoad)[:0]
				}
				for !in.IsDelim(']') {
					var v1 int
					v1 = int(in.Int())
					out.PayLoad = append(out.PayLoad, v1)
					in.WantComma()
				}
				in.Delim(']')
			}
		default:
			in.SkipRecursive()
		}
		in.WantComma()
	}
	in.Delim('}')
	if isTopLevel {
		in.Consumed()
	}
}
func easyjson6a975c40EncodeCh471(out *jwriter.Writer, in Request) {
	out.RawByte('{')
	first := true
	_ = first
	{
		const prefix string = ",\"transaction_id\":"
		if first {
			first = false
			out.RawString(prefix[1:])
		} else {
			out.RawString(prefix)
		}
		out.String(string(in.TransactionID))
	}
	{
		const prefix string = ",\"payload\":"
		if first {
			first = false
			out.RawString(prefix[1:])
		} else {
			out.RawString(prefix)
		}
		if in.PayLoad == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 {
			out.RawString("null")
		} else {
			out.RawByte('[')
			for v2, v3 := range in.PayLoad {
				if v2 > 0 {
					out.RawByte(',')
				}
				out.Int(int(v3))
			}
			out.RawByte(']')
		}
	}
	out.RawByte('}')
}

// MarshalJSON supports json.Marshaler interface
func (v Request) MarshalJSON() ([]byte, error) {
	w := jwriter.Writer{}
	easyjson6a975c40EncodeCh471(&w, v)
	return w.Buffer.BuildBytes(), w.Error
}

// MarshalEasyJSON supports easyjson.Marshaler interface
func (v Request) MarshalEasyJSON(w *jwriter.Writer) {
	easyjson6a975c40EncodeCh471(w, v)
}

// UnmarshalJSON supports json.Unmarshaler interface
func (v *Request) UnmarshalJSON(data []byte) error {
	r := jlexer.Lexer{Data: data}
	easyjson6a975c40DecodeCh471(&r, v)
	return r.Error()
}

// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
func (v *Request) UnmarshalEasyJSON(l *jlexer.Lexer) {
	easyjson6a975c40DecodeCh471(l, v)
}

optimization_test.go

package profiling

import "testing"

func TestCreateRequest(t *testing.T) {
	str := createRequest()
	t.Log(str)
}

func TestProcessRequest(t *testing.T) {
	reqs := []string{}
	reqs = append(reqs, createRequest())
	reps := processRequest(reqs)
	t.Log(reps[0])
}

func BenchmarkProcessRequest(b *testing.B) {

	reqs := []string{}
	reqs = append(reqs, createRequest())
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_ = processRequest(reqs)
	}
	b.StopTimer()

}

以上的代码主要是对比使用easyjson与go原装的json.Unmarshal,使用性能调优工具直观的看出easyjson的性能更佳。

2. 调优步骤

1. 生成cpu.prof文件

## benchmark不理解的在博客中寻找讲golang测试的章节,有队benchmark对一个介绍。
go test -bench=. -cpuprofile=cpu.prof
go test -bench=. -memprofile=mem.prof

2. 执行pprof调优

1. cpu调优示例
go tool pprof cpu.prof
File: ch47.test
Type: cpu
Time: Aug 2, 2020 at 2:27pm (BST)
Duration: 1.41s, Total samples = 1.25s (88.59%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) list processRequest
Total: 1.25s
ROUTINE ======================== go_learning/code/ch47.processRequest in /root/codes/go/src/go_learning/code/ch47/optmization.go
      30ms      1.04s (flat, cum) 83.20% of Total
         .          .     20:}
         .          .     21:
         .          .     22:func processRequest(reqs []string) []string {
         .          .     23:	reps := []string{}
         .          .     24:	for _, req := range reqs {
         .       10ms     25:		reqObj := &Request{}
         .      140ms     26:		reqObj.UnmarshalJSON([]byte(req))
         .      320ms     27:		json.Unmarshal([]byte(req), reqObj)
         .          .     28:
         .          .     29:		var buf strings.Builder
         .          .     30:		var ret string
         .          .     31:		for _, e := range reqObj.PayLoad {
      20ms      290ms     32:			ret += strconv.Itoa(e) + ","
         .       50ms     33:			buf.WriteString(strconv.Itoa(e))
         .          .     34:			buf.WriteString(",")
         .          .     35:		}
      10ms       30ms     36:		repObj := &Response{reqObj.TransactionID, buf.String()}
         .          .     37:		repObj = &Response{reqObj.TransactionID, ret}
         .       20ms     38:		repJson, err := repObj.MarshalJSON()
         .      160ms     39:		repJson, err = json.Marshal(&repObj)
         .          .     40:		if err != nil {
         .          .     41:			panic(err)
         .          .     42:		}
         .       20ms     43:		reps = append(reps, string(repJson))
         .          .     44:	}
         .          .     45:	return reps
         .          .     46:}

从结果上来看可见processRequest 这个方法的CPU占用的总时间是1.26s ,其中占用时间最长的是json.Unmarshal([]byte(req), reqObj),使用了680ms,这也是golang自己的官方lib中的方法json.Unmarshal,相较于上面的reqObj.UnmarshalJSON([]byte(req))用的时间(230ms),用easyJson去做反序列化,看出性能上是快了不少,几乎是两倍的差距。第二个占用较多cpu资源的就是ret += strconv.Itoa(e) + ",",使用string直接拼接的方式确实是不赞同,尤其是在大量字符串拼接的场景下,对cpu和内存都是比较大的消耗,具体原因在下面的内存调优中会解释。

2.内存调优示例
go tool pprof mem.prof
File: ch47.test
Type: alloc_space
Time: Aug 2, 2020 at 2:13pm (BST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) list processRequest
Total: 32.51MB
ROUTINE ======================== go_learning/code/ch47.processRequest in /root/codes/go/src/go_learning/code/ch47/optmization.go
   28.01MB    32.51MB (flat, cum)   100% of Total
         .          .     21:
         .          .     22:func processRequest(reqs []string) []string {
         .          .     23:	reps := []string{}
         .          .     24:	for _, req := range reqs {
         .          .     25:		reqObj := &Request{}
       1MB     3.50MB     26:		reqObj.UnmarshalJSON([]byte(req))
    1.50MB     1.50MB     27:		json.Unmarshal([]byte(req), reqObj)
         .          .     28:
         .          .     29:		var buf strings.Builder
         .          .     30:		var ret string
         .          .     31:		for _, e := range reqObj.PayLoad {
   25.51MB    25.51MB     32:			ret += strconv.Itoa(e) + ","
         .        1MB     33:			buf.WriteString(strconv.Itoa(e))
         .          .     34:			buf.WriteString(",")
         .          .     35:		}
         .          .     36:		repObj := &Response{reqObj.TransactionID, buf.String()}
         .          .     37:		repObj = &Response{reqObj.TransactionID, ret}
         .          .     38:		repJson, err := repObj.MarshalJSON()
         .        1MB     39:		repJson, err = json.Marshal(&repObj)
         .          .     40:		if err != nil {
         .          .     41:			panic(err)
         .          .     42:		}
         .          .     43:		reps = append(reps, string(repJson))
         .          .     44:	}

从结果上看最消耗内存的是ret += strconv.Itoa(e) + ",",这个要比较好理解,go的字符串拼接会消耗很大的内存,go的string底层其实是一个slice,熟悉go的slice原理的都知道,slice的append操作是要另寻一块连续的内存地址的,正因为要另开辟一块连续的内存空间才使内存消耗看起来如此巨大,这里采取的策略是使用buffer的WriteString方法,go的buffer的扩充方式有三种,这里不依依赘述了,以后我会开专栏去讲解的go的buffer,在这里只是提供了一种解决方案而已,本篇的核心思想是提现如何去调试你的代码的性能。

4. 总结

性能调优其实是一门很大的学问,对于程序员本身来说要求比较高,除了熟练的掌握语言本身以外,还必须要有一定的经验,这样才能在调试中快速的定位问题以及处理问题。

posted @ 2021-02-10 16:05  ttlv  阅读(451)  评论(0编辑  收藏  举报