使用 defer 还是不使用 defer?

栏目: Go · 发布时间: 5年前

内容简介:对于Go语言的defer语句,或许你回经历一个Go语言增加的比如下面的代码:

对于 Go 语言的defer语句,或许你回经历一个 赞赏 --> 怀疑 --> 肯定 --> 再怀疑 的一个过程,本文带你回顾一下defer的故事,以及如何在代码中使用defer语句。

最初的故事

Go语言增加的 defer 语句在简化代码方面确实用处多多, 尤其是对资源的释放等场景,提供了简便的代码方法。其实其它语言也有类似的语法或者语法糖, 比如 Java 就有 try-with-resource 语句,可以自动释放实现 java.io.Closeable 的对象。

比如下面的代码:

func foo(bar []string) {
	mu.Lock()
	defer mu.Unlock()

	if len(bar) ==0 {
		return
	}

	for _, s := range bar {
		if !strings.HasPrefix(s, "https://") {
			return
		}
	}


	......
}

如果不使用 defer , 代码中可能需要出现多次重复的对同一个资源的清理释放的方法调用。

func foo(bar []string) {
	mu.Lock()

	if len(bar) ==0 {
		mu.Unlock()
		return
	}

	for _, s := range bar {
		if !strings.HasPrefix(s, "https://") {
			mu.Unlock()
			return
		}
	}


	......

	mu.Unlock()
}

相比较而言,第一个代码看起来比较好,锁的获取和释放成对出现,没有冗余的代码,锁的延迟释放和锁的获取紧挨着,不会忘记释放锁或者重复释放锁。

所以, 你会在很多Go的项目和库中看到 defer 的使用,而且在Go的标准库中也大量的使用(在go 1.11.2的标准库中,大约有4400多次的defer调用)。

使用defer是有代价的

随着你对Go语言的熟悉,你也许在性能测试中发现defer语句对性能的影响,也许你也阅读过一些文章, 比如雨痕的 Go 性能优化技巧 4/10 ,对defer语句带来的额外开销有一些测试。

下面是对多个defer情况的性能测试:

package test

import (
	"sync"
	"testing"
)

var mu sync.Mutex

//go:noinline
func foo() {}

//go:noinline
func deferLockTwo() {
	mu.Lock()
	defer mu.Unlock()
	defer foo()
}

//go:noinline
func deferLock() {
	mu.Lock()
	defer mu.Unlock()
	foo()
}

//go:noinline
func deferLockClosure() {
	mu.Lock()
	defer func() { mu.Unlock() }()
	foo()
}

//go:noinline
func noDeferLock() {
	mu.Lock()
	mu.Unlock()
	foo()
}

func BenchmarkDeferLockTwo(b *testing.B) {
	for i :=0; i < b.N; i++ {
		deferLockTwo()
	}
}

func BenchmarkDeferLock(b *testing.B) {
	for i :=0; i < b.N; i++ {
		deferLock()
	}
}

func BenchmarkDeferLockClosure(b *testing.B) {
	for i :=0; i < b.N; i++ {
		deferLockClosure()
	}
}

func BenchmarkNoDeferLock(b *testing.B) {
	for i :=0; i < b.N; i++ {
		noDeferLock()
	}
}

测试结果:

BenchmarkDeferLockTwo-4       	20000000	        89.8 ns/op
BenchmarkDeferLock-4          	20000000	        70.4 ns/op
BenchmarkDeferLockClosure-4   	20000000	        67.6 ns/op
BenchmarkNoDeferLock-4        	100000000	        19.3 ns/op

可以看到,直接的请求释放锁只需要 19.3 纳秒,可是如果通过 defer 释放锁,却需要 70.4 纳秒。

比较有意思的是,不通过 defer mu.Unlock() ,而是通过 Closure 的方式释放锁,性能会比 defer mu.Unlock() 好那么一点点。

如果代码中有多个defer, 耗费的时间更长。

可以看到,代码中使用defer, 可能会给程序的性能代码几十纳秒的开销(根据运行环境的不同,数值有所不同)。

当然, 你可以认为,几十纳秒的开销对于我的应用影响不大,一个实际的业务耗费的时间都有100毫秒,所以这个这点时间损耗不算什么。如果实际观察(比如通过pprof trace)defer语句没有影响到你的性能,那么一切还好,但是对于一个负载比较大的机器,对于 hot path 上的代码,可能需要goroutine竞争的代码,需要对性能进行进一步的优化,还是需要考虑避免对 defer 滥用。

hot paths are code execution paths in the compiler in which most of the execution time is spent, and which are potentially executed very often.

by Konrad Rudolph

当然对于 Mutex 来说, 尽早的释放锁,在临界区结束之后, 而不是在函数返回时才释放锁是我们掌握的一个基本常识, 这样能避免无谓的过长的锁。

不在循环中使用defer也应该是我们掌握的另外一个常识, 因为循环可能产生多个defer语句,性能差,而且defer又会使资源过晚的释放。

Go编译器使用 runtime.deferproc 注册延迟调用,除了这个延迟调用的函数地址外,还会复制函数参数,在当前函数返回时,再通过 runtime.deferreturn 提取相关信息执行延迟调用, 这显然要比直接的一个函数调用指令要麻烦,也难怪性能回下降。 同时,也说明了 Closure 方式比 defer mu.Unlock() 性能要好那么一点点,因为 Closure 方式的延迟函数没有参数。

defer实现的优化和现实

Go 的代码库中也有讨论 defer 慢的issue, 在2016年曾经热烈讨论过; runtime: defer is slow , 当时有一些项目开始注意这个问题,开始将项目中的一些defer替换成直接锁的释放, 比如 prometheusx/time/rate

@aclements 对此进行了优化,在 Go 1.8中, defer性能提高了一倍, 当然@aclements承认defer还有优化的空间,但是目前并没有强烈的优化的意愿,除非有测试数据的支持。

@josharian也提供一个case, 他唯一一次的优化是实现一个tiny routines时候,因为涉及到了mutex, 避免过长的竞争所以避免使用 defer

@rhysh 也提供了一个实际的数据,他在实现一个https服务器,可以观察到 crypto/tlsinternal/poll 的defer代码回很稳定的占用几个百分点的cpu占用, 至少,香标准库中下面的 代码 可以进行优化:

func (c *Conn) Write(b []byte) (int, error) {
	// interlock with Close below
	for {
		x := atomic.LoadInt32(&c.activeCall)
		if x&1 !=0 {
			return0, errClosed
		}
		if atomic.CompareAndSwapInt32(&c.activeCall, x, x+2) {
			defer atomic.AddInt32(&c.activeCall,-2) // 这里
			break
		}
    }
    
    ......

总的来说, defer 不是免费的,但是也不是那么不堪,除非你的代码是是要频繁执行的代码,需要进行进一步的优化,可以考虑去掉defer而采用手工执行, 否则在代码中使用 defer 并不是一个问题。 从实践上,多观察pprof的监控,看看defer是不是在你的 hot path 之中。

参考资料

  1. https://github.com/golang/go/issues/14939
  2. https://medium.com/i0exception/runtime-overhead-of-using-defer-in-go-7140d5c40e32
  3. https://blog.learngoprogramming.com/gotchas-of-defer-in-go-1-8d070894cb01
  4. https://github.com/golang/go/issues/6980
  5. https://github.com/golang/go/issues/20240
  6. https://go-review.googlesource.com/c/time/+/29379/5/rate/rate.go
  7. https://segmentfault.com/a/1190000005027137

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

C陷阱与缺陷

C陷阱与缺陷

Andrew Koenig / 高巍 / 人民邮电出版社 / 2003-12-1 / 30.00

作者以自己1985年在Bell实验室时发表的一篇论文为基础,结合自己的工作经验扩展成为这本对C程序员具有珍贵价值的经典著作。写作本书的出发点不是要批判C语言,而是要帮助C程序员绕过编程过程中的陷阱和障碍。作者以自己1985年在Bell实验室时发表的一篇论文为基础,结合自己的工作经验扩展成为这本对C程序员具有珍贵价值的经典著作。写作本书的出发点不是要批判C语言,而是要帮助C程序员绕过编程过程一起来看看 《C陷阱与缺陷》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具