40

优雅的读取http请求或响应的数据-续

 5 years ago
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 序列化的问题。

之前我们优化了两个部分, jsonioutil.ReadAll , 先对比 ioutil.ReadAll , 这里测试的代码分成两个部分做对比,一部分单纯对比 ioutil.ReadAllio.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 倍,内存分配也很少,而 IoCopyAndJsonIouitlReadAllAndJson 的效率差异极小仅有 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
}

这样一来我们使用 bufferjson 序列化优化效果就大打折扣,甚至适得其反了。

再次感谢 “wxe” 网友的提问,这里没有使用实际的应用场景做性能测试,主要发现在性能测试中使用 http 服务会导致 connect: can't assign requested address 问题,所以测试用使用了函数模拟,如果有朋友有更好的测试方法欢迎一起交流。

谢谢你请我吃糖果

VzmYj2R.jpg!web支付宝

uaMJf2b.jpg!web微信


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK