Go 高性能之 timer 优化

概述 #

time.Aftertime.Tick 不同,是一次性触发的,触发后 timer 本身会从时间堆中删除。 所以一般情况下直接用 <-time.After 是没有问题的, 不过在 for 循环的时候要注意:

每次分配新的 timer #

package performance

import (
	"testing"
	"time"
)

func Benchmark_Timer(b *testing.B) {
	for i := 0; i < b.N; i++ {
		select {
		case <-time.After(time.Millisecond):    // 每次生成新的 timer
		}
	}
}

运行测试,并将基准测试结果写入文件:

# 运行 1000 次,统计内存分配
$ go test -run='^$' -bench=. -count=1 -benchtime=1000x -benchmem > slow.txt

复用一个 timer #

刚才的示例代码中,每次进入 selecttime.After 都会分配一个新的 timer。 因此会在短时间内创建大量的 timer,虽然 timer 在触发后会消失,但这种写法会造成无意义的 cpu 资源浪费。 正确的写法应该对 timer 进行复用。

package performance

import (
	"testing"
	"time"
)

func Benchmark_Timer(b *testing.B) {
	timer := time.NewTimer(time.Second)

	for i := 0; i < b.N; i++ {
		timer.Reset(time.Millisecond) // 复用一个 timer
		select {
		case <-timer.C:
		}
	}
}

运行测试,并将基准测试结果写入文件:

# 运行 1000 次,统计内存分配
$ go test -run='^$' -bench=. -count=1 -benchtime=1000x -benchmem > fast.txt

使用 benchstat 比较差异 #

$ benchstat -alpha=100 fast.txt slow.txt 

# 输出如下:
name      old time/op    new time/op    delta
_TImer-8    1.27ms ± 0%    1.27ms ± 0%  +0.35%  (p=1.000 n=1+1)

name      old alloc/op   new alloc/op   delta
_TImer-8     0.00B        200.00B ± 0%   +Inf%  (p=1.000 n=1+1)

name      old allocs/op  new allocs/op  delta
_TImer-8      0.00           3.00 ± 0%   +Inf%  (p=1.000 n=1+1)

输出的结果分为了三行,分别对应基准期间的: 运行时间、内存分配总量、内存分配次数,采用了 复用 timer 方案后:

  • 内存分配总量降至 0
  • 内存分配次数降至 0

因为时间关系,基准测试只运行了 1000 次,运行次数越大,优化的效果越明显。感兴趣的读者可以将 -benchtime 调大后看看优化效果。

小结 #

通过复用 time.After 可以显著改善内存占用情况,在计时器比较多的业务场景中,还可以提升性能。

转载申请

本作品采用 知识共享署名 4.0 国际许可协议 进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,商业转载请联系作者获得授权。

© 蛮荆 | 陕公网安备 61011302001681 号 | 陕ICP备2023004378号-1 | Rendered by Hugo