优雅的读取http请求或响应的数据-续
source link: https://blog.thinkeridea.com/201902/go/you_ya_de_du_qu_http_qing_qiu_huo_xiang_ying_de_shu_ju_2.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
之前发布 【Go】优雅的读取http请求或响应的数据续 文章,网友 “wxe” 咨询:“优化前后的请求耗时变化有多大”,之前只分析了内存分配,这篇文章用单元测试的方式分析优化前后的耗时情况, 本文源码 。
非常感谢 “wxe” 网友的提问,让我在测试过程中发现一个 json
序列化的问题。
之前我们优化了两个部分, json
与 ioutil.ReadAll
, 先对比 ioutil.ReadAll
, 这里测试的代码分成两个部分做对比,一部分单纯对比 ioutil.ReadAll
和 io.Copy
+ sync.Pool
,另一部分增加 jsoniter.Unmarshal
来延迟 pool.Put(buffer)
的执行, 源码 。
package iouitl_readall import ( "bytes" "io" "io/ioutil" "sync" jsoniter "github.com/json-iterator/go" ) var pool = sync.Pool{ New: func() interface{} { return bytes.NewBuffer(make([]byte, 4096)) }, } func IoCopyAndJson(r io.Reader) error { buffer := pool.Get().(*bytes.Buffer) buffer.Reset() defer pool.Put(buffer) res := Do(r) _, err := io.Copy(buffer, res) if err != nil { return err } m := map[string]string{} err = jsoniter.Unmarshal(buffer.Bytes(), &m) return err } func IouitlReadAllAndJson(r io.Reader) error { res := Do(r) data, err := ioutil.ReadAll(res) if err != nil { return err } m := map[string]string{} err = jsoniter.Unmarshal(data, &m) return err } func IoCopy(r io.Reader) error { buffer := pool.Get().(*bytes.Buffer) buffer.Reset() defer pool.Put(buffer) res := Do(r) _, err := io.Copy(buffer, res) if err != nil { return err } return err } func IouitlReadAll(r io.Reader) error { res := Do(r) data, err := ioutil.ReadAll(res) if err != nil { return err } _ = data return err }
测试代码如下 源码 :
package iouitl_readall import ( "bytes" "testing" ) var data = bytes.Repeat([]byte("ABCD"), 1000) func BenchmarkIouitlReadAll(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { err := IouitlReadAll(bytes.NewReader(data)) if err != nil { b.Error(err.Error()) } } }) } func BenchmarkIoCopy(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { err := IoCopy(bytes.NewReader(data)) if err != nil { b.Error(err.Error()) } } }) } func BenchmarkIouitlReadAllAndJson(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { err := IouitlReadAllAndJson(bytes.NewReader(data)) if err != nil { b.Error(err.Error()) } } }) } func BenchmarkIoCopyAndJson(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { err := IoCopyAndJson(bytes.NewReader(data)) if err != nil { b.Error(err.Error()) } } }) }
测试结果如下:
goos: darwin goarch: amd64 pkg: github.com/thinkeridea/example/iouitl_readall BenchmarkIouitlReadAll-8 500000 2752 ns/op 14496 B/op 6 allocs/op BenchmarkIoCopy-8 20000000 65.2 ns/op 48 B/op 1 allocs/op BenchmarkIouitlReadAllAndJson-8 100000 20022 ns/op 46542 B/op 616 allocs/op BenchmarkIoCopyAndJson-8 100000 17615 ns/op 32102 B/op 611 allocs/op
结论:
可以发现 IoCopy
方法是 IouitlReadAll
方法效率的 40 倍,内存分配也很少,而 IoCopyAndJson
和 IouitlReadAllAndJson
的效率差异极小仅有 2407ns
,大约是 1.13倍,不过内存分配还是少了很多的,为什么会这样呢,这就是 sync.Pool
的导致的, sync.Pool
每次获取使用时间越短,命中率就越高,就可以减少创建新的缓存,这样效率就会大大提高,而 jsoniter.Unmarshal
很耗时,就导致 sync.Pool
的命中率降低了,所以性能下降极其明显.
使用 io.Copy
+ sync.Pool
表面上执行效率不会有很大提升,但是会大幅度减少内存分配,从而可以减少 GC
的负担,在单元测试中我们并没有考虑 GC
的问题,而 GC
能带来的性能提升会更有优势。
在看一下 json
使用 sync.Pool
的效果吧 源码
package iouitl_readall import ( "bytes" "encoding/json" jsoniter "github.com/json-iterator/go" ) func Json(r map[string]string) error { data, err := json.Marshal(r) if err != nil { return err } _ = data return nil } func JsonPool(r map[string]string) error { buffer := pool.Get().(*bytes.Buffer) buffer.Reset() defer pool.Put(buffer) e := json.NewEncoder(buffer) err := e.Encode(r) if err != nil { return err } return nil } func JsonIter(r map[string]string) error { data, err := jsoniter.Marshal(r) if err != nil { return err } _ = data return nil } func JsonIterPool(r map[string]string) error { buffer := pool.Get().(*bytes.Buffer) buffer.Reset() defer pool.Put(buffer) e := jsoniter.NewEncoder(buffer) err := e.Encode(r) if err != nil { return err } return nil }
性能测试代码 源码 :
package iouitl_readall import ( "strconv" "strings" "testing" ) var request map[string]string func init() { request = make(map[string]string, 100) for i := 0; i < 100; i++ { request["X"+strconv.Itoa(i)] = strings.Repeat("A", i/2) } } func BenchmarkJson(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { err := Json(request) if err != nil { b.Error(err.Error()) } } }) } func BenchmarkJsonIter(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { err := JsonIter(request) if err != nil { b.Error(err.Error()) } } }) } func BenchmarkJsonPool(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { err := JsonPool(request) if err != nil { b.Error(err.Error()) } } }) } func BenchmarkJsonIterPool(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { err := JsonIterPool(request) if err != nil { b.Error(err.Error()) } } }) }
测试结果如下:
goos: darwin goarch: amd64 pkg: github.com/thinkeridea/example/iouitl_readall BenchmarkJson-8 100000 13297 ns/op 13669 B/op 207 allocs/op BenchmarkJsonPool-8 100000 13310 ns/op 10218 B/op 206 allocs/op BenchmarkJsonIter-8 500000 2948 ns/op 3594 B/op 4 allocs/op BenchmarkJsonIterPool-8 200000 6126 ns/op 6040 B/op 144 allocs/op PASS ok github.com/thinkeridea/example/iouitl_readall 12.716s
这里使用了两个 json
包, 一个是标准库的,一个是 jsoniter
(也是社区反馈效率最高的),对比两个包使用 sync.Pool
和不使用之间的差异,发现标准库 json
包使用后内存有少量减少,但是运行效率稍微下降了,差异不是很大, jsoniter
包差异之所谓非常明显,发现使用 sync.Pool
之后不仅内存分配更多了,执行效率也大幅度下降,差了将近3倍有余。
是不是很奔溃,这是啥情况 jsoniter
本身就使用了 sync.Pool
作缓冲,我们使用 jsoniter.NewEncoder(buffer)
创建一个序列化实例,但是其内部并没有直接使用 io.Writer
而是先使用缓冲序列化数据,之后写入 io.Writer
, 具体代码如下:
// Flush writes any buffered data to the underlying io.Writer. func (stream *Stream) Flush() error { if stream.out == nil { return nil } if stream.Error != nil { return stream.Error } n, err := stream.out.Write(stream.buf) if err != nil { if stream.Error == nil { stream.Error = err } return err } stream.buf = stream.buf[n:] return nil }
这样一来我们使用 buffer
做 json
序列化优化效果就大打折扣,甚至适得其反了。
再次感谢 “wxe” 网友的提问,这里没有使用实际的应用场景做性能测试,主要发现在性能测试中使用 http
服务会导致 connect: can't assign requested address
问题,所以测试用使用了函数模拟,如果有朋友有更好的测试方法欢迎一起交流。
谢谢你请我吃糖果
支付宝
微信
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK