关于fmt包Fprint系列方法的性能问题

关于fmt包Fprint系列方法的性能问题

作者:水不要鱼

(注:能力有限,如有说错,请指正!)

(原文发表在我的个人网站中:https://www.fishin.com.cn/blog/article.html?articleId=5)

最近在使用 Go 语言实现一个日志库 (logit) 的时候,发现了一个性能问题,经过 Go 的 cpuprofile 检测之后,
问题出在 fmt.Fprintf 中,于是我写了一个简单的测试案例,看看 Fprintf 的性能究竟比直接 writer.Write 差多少:

// 测试使用的 writer,不进行任何输出,避免 I/O 带来的测试波动
type nopWriter struct{}
func (w *nopWriter) Write(p []byte) (n int, err error) {
    return 0, nil
}

// 测试 Fmt.Fprintf 的性能
func BenchmarkFmtFprintf(b *testing.B) {

    w := &nopWriter{}
    for i := 0; i < b.N; i++ {
        fmt.Fprintf(w, "i")
    }
}

// 测试直接 Write 的性能
func BenchmarkWriter(b *testing.B) {

    w := &nopWriter{}
    for i := 0; i < b.N; i++ {
        w.Write([]byte("i"))
    }
}

测试结果如下

fmtFprintf 的性能检测:
fmtFprintf

直接 Write 的性能检测:
直接 Write
什么!!!两个居然差了 126 倍。。。好奇心驱使我开始了源码探索:

// Fprintf formats according to a format specifier and writes to w.
// It returns the number of bytes written and any write error encountered.
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
	p := newPrinter()
	p.doPrintf(format, a) // 对!就是它!这个方法耗时相当高!点进去一探究竟!
	n, err = w.Write(p.buf)
	p.free()
	return
}

Cpu profile 告诉我,doPrintln 方法耗时相当严重,于是,我继续跟进去看源码,但是这个源码很长,
这里就只贴一个重点的代码,就是 printArg 方法:

func (p *pp) printArg(arg interface{}, verb rune) {
    p.arg = arg
	p.value = reflect.Value{}

	if arg == nil {
		switch verb {
		case 'T', 'v':
			p.fmt.padString(nilAngleString)
		default:
			p.badVerb(verb)
		}
		return
	}

	// Special processing considerations.
	// %T (the value's type) and %p (its address) are special; we always do them first.
	switch verb {
	case 'T':
		p.fmt.fmtS(reflect.TypeOf(arg).String())
		return
	case 'p':
		p.fmtPointer(reflect.ValueOf(arg), 'p')

    // 。。。。省略以下代码,因为答案已经很明显了!
}

上面的代码只看了一小部分就已经知道问题所在了,没错,就是 reflect 反射包的使用。
这个方法短短几行中就存在好几个反射操作,能不慢嘛。。。

不过,仔细一想,也对,fmt 包中的方法,比如 Println,参数都是 interface{} 的,这是为了通用化,
那内部就必然需要判断实际的类型然后做出实际的输出操作。而我的日志库 (logit)
就是输出一个字符串,其实就没必要做这个通用的判断。于是我想起了 io.WriteString 方法,立马测试一番!

// 测试 io.WriteString 性能
func BenchmarkIoWriteString(b *testing.B) {

    w := &nopWriter{}
    for i := 0; i < b.N; i++ {
        io.WriteString(w, "i")
    }
}

io.WriteString 的性能检测:
io.WriteString

哈???真是让人大跌眼镜,这货的性能居然和 fmt.Fprintf 差不多?莫非又存在反射操作?上源码!

// WriteString writes the contents of the string s to w, which accepts a slice of bytes.
// If w implements StringWriter, its WriteString method is invoked directly.
// Otherwise, w.Write is called exactly once.
func WriteString(w Writer, s string) (n int, err error) {
	if sw, ok := w.(StringWriter); ok {
		return sw.WriteString(s)
	}
	return w.Write([]byte(s))
}

这货的源码很简单,如果是 StringWriter 接口的实现,就使用这个实现的 WriteString 方法,
否则直接 Write。而我的测试用例中,nopWriter 明显没有实现 StringWriter 接口,所以断言失败,
直接调用 Write 方法。那性能不应该这么差啊!莫非是断言导致的?不管三七二十一,测试一番再说!

// 测试断言性能
func BenchmarkAssert(b *testing.B) {

    var w interface{} = &nopWriter{}
    for i := 0; i < b.N; i++ {
        if ww, ok := w.(*nopWriter); ok {
            ww.Write([]byte("i"))
        }
    }
}

断言的性能检测:
断言

从结果来看,好像并不是断言导致的,那究竟是什么原因咧。。。这个我是真不知道,如果有知道的小伙伴,赶紧评论一番告诉我哈,小弟感激不尽!

不过话说回来,实际应用中不可能使用 nopWriter 这样的 IO 设备,更多是文件之类的操作,所以我们来看看如果是实际的文件 IO 的话,性能是怎么样的:

// 测试文件 I/O 的性能
func BenchmarkFileIo(b *testing.B) {

    file, _ := os.Create("Z:/test.txt")

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        file.Write([]byte("i"))
    }
}

file IO 的性能检测:
file IO

注意:这里的 Z:/ 盘实际上还是内存虚拟出来的磁盘,写入速度比普通硬盘快不少。

哇塞,果然实际的 IO 操作耗时一下就上去了。很显然,在这种情况即使是耗时为 40ns
的 fmt.Fprintf 方法也才只是 IO 操作的零头,可以说对 IO 性能的影响不大,可以忽略不计。

总的来说,fmt 包中的一些方法使用了很多反射操作,虽然比较耗时,但是实际上在使用这些方法时,
真正影响性能的并不是方法本身,而是 IO 操作,所以具体情况还是要具体处理,不能一篮子把这些方法打死,更不能过度优化,俗话说过度或过早优化是万恶之源 😃。

发布了10 篇原创文章 · 获赞 0 · 访问量 503
App 阅读领勋章
微信扫码 下载APP
阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 精致技术 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览