Java与Golang时间戳的奥秘

最近使用开发一款Golang库,发现通过官方库time.Now()获取系统时间戳时,会存在一点性能问题,在分析其内部实现,及对比Java语言时间戳实现及性能指标后,封装了一个相对更加高效率的时间戳函数,本文简单介绍这个经历。

以下具体的benchmark结果采用相同的硬件资源,即我自己的MacBook Pro (15-inch, 2018)

顺便说一句,2018款MBP的键盘非常垃圾,强烈建议不要买。

性能问题

以下为简单的benchmark代码:

1
2
3
4
5
6
7
func BenchmarkNowMicrosecond(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = time.Now().Unix() / 1e3
}
}

其最终结果为:

1
BenchmarkNowMicrosecond-12    	20000000	   68.0 ns/op     0 B/op     0 allocs/op

而Java语言中获取时间戳的性能指标为(参见Github):

1
2
Benchmark               Mode  Cnt   Score   Error  Units
TimestampBenchmark.now avgt 9 25.697 ± 0.139 ns/op

可以很明显的观察到,time.Now()的性能相比System.currentTimeMillis()差距较大,对于普通的业务系统而言,43ns没什么大不了,但是对于偏底层的通用性组件还是比较敏感的。

剖析原因

Java语言中System.currentTimeMillis()的实现是这样的:

1
2
3
4
5
6
jlong os::javaTimeMillis() {
timeval time;
int status = gettimeofday(&time, NULL);
assert(status != -1, "bsd error");
return jlong(time.tv_sec) * 1000 + jlong(time.tv_usec / 1000);
}

Go语言中time.Now()的实现调用了asm实现的time()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
TEXT runtime·walltime(SB),NOSPLIT,$20
MOVL $0, 0(SP) // real time clock
LEAL 8(SP), AX
MOVL AX, 4(SP) // timespec
NACL_SYSCALL(SYS_clock_gettime)
MOVL 8(SP), AX // low 32 sec
MOVL 12(SP), CX // high 32 sec
MOVL 16(SP), BX // nsec

// sec is in AX, nsec in BX
MOVL AX, sec_lo+0(FP)
MOVL CX, sec_hi+4(FP)
MOVL BX, nsec+8(FP)
RET

TEXT syscall·now(SB),NOSPLIT,$0
JMP runtime·walltime(SB)

我对太偏底层的部分研究的不太深入,看起来两者分别依赖于gettimeofdayclock_gettime函数,根据我粗浅的理解,后者在精度方面更高一些,但是性能上存在劣势。

采用syscall.Gettimeofday()

既然原因找到了,在Go语言中也采用gettimeofday会如何呢?

官方库通过syscall.Gettimeofday()封装了这个函数,它的调用链路是syscall->asm->libc_gettimeofday,我感觉它与System.currentTimeMillis逻辑应该一样。

syscall.Gettimeofday()进行性能测试:

1
2
3
4
5
6
7
8
func BenchmarkCurrentSecond(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
var tv syscall.Timeval
_ = syscall.Gettimeofday(&tv)
}
}

最终性能指标为:

1
BenchmarkCurrentSecond-12     30000000	  40.0 ns/op	    0 B/op     0 allocs/op

看起来这种方案在性能上仍然不如java,但是差距已不太大。

我同样尝试过cgo直接调用gettimeofday,但是其性能指标更差,即超过80ns/op

etime

以上相关测试代码可以在etime可以看到,这个库是我专门封装用于复用的time扩展库,用法简单:

1
go get github.com/go-eden/etime

直接调用函数获得当前毫秒微秒时间戳:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"github.com/go-eden/etime"
"time"
)

func main() {
println(etime.CurrentSecond())
println(etime.CurrentMillisecond())
println(etime.CurrentMicrosecond())

println(time.Now().Unix())
println(time.Now().UnixNano() / 1e6)
println(time.Now().UnixNano() / 1e3)
}

需要额外说明的是,我拓展的这些函数性能上存在一些性能优化,但是精度上可能不如time.Now()

如果你对纳秒级的时间精度很敏感的话,建议不要使用它。