【JS时间戳】获取时间戳的最快方式探究
TAG
nodejs,nodejs时间戳,js时间戳,timestamp,date.now,performance.now,时间戳,小数取整,位运算,精度丢失,数字的存储方式,小数的二进制存储,位运算的限制
获取13位时间戳方法及性能简单对比
以前获取时间戳没什么太认真过,今天突然突发奇想,哪种方式获取时间戳最快呢?特别是常用的10位时间戳。然后了解到获取时间戳的方式有很多种,比如网上常用的下面几种方式(除了第一种):
// 下列速度依次递减
performance.timeOrigin + performance.now()
Date.now()
new Date().getTime()
new Date().valueOf()
Date.parse(new Date())
// 下列两个方法获取时间差等
process.uptime()
process.hrtime()
通过如下代码进行验证:
const performance = require('perf_hooks').performance;
let s, e, interval = 10000000
console.log(`获取${interval}次时间戳速度对比:====================================`)
s = process.uptime()
for (let i = 0; i < interval; i++) performance.timeOrigin + performance.now()
e = process.uptime()
console.log('performance.timeOrigin+performance.now():', performance.timeOrigin + performance.now(), e - s)
s = process.uptime()
for (let i = 0; i < interval; i++) Date.now()
e = process.uptime()
console.log('Date.now():', Date.now(), e - s)
s = process.uptime()
for (let i = 0; i < interval; i++) new Date().getTime()
e = process.uptime()
console.log('new Date().getTime()', new Date().getTime(), e - s)
s = process.uptime()
for (let i = 0; i < interval; i++) Date.parse(new Date())
e = process.uptime()
console.log('Date.parse(new Date())', Date.parse(new Date()), e - s)
结果如下:
获取10000000次时间戳速度对比:====================================
performance.timeOrigin+performance.now(): 1596589863553.657 0.47400000000000003
Date.now(): 1596589864435 0.8799999999999999
new Date().getTime() 1596589866093 1.6569999999999998
Date.parse(new Date()) 1596589887000 21.115000000000002
除了第一种performance
之外,其他几种方式网上的比对一大堆,大家就自行了解啦。起初我也是以为Date.now()
是最快的,但是当带着好奇去了解的时候,突然发现了这个performance
,然后一测试发现了新大陆!关于performance
的介绍,请看我另外一篇转载的文章:《解读 Nodejs 性能 API:Performance Timing》
这几种方式的对比这里就不再赘述了,由上往下速度递减,performance完胜,更多详细对比网上一大堆。但是如果说获取时间戳,基本都是精确到毫秒的13位时间戳(除了Date.parse)。但是日常开发中很多时候用到的是10位时间戳,那么获取10位时间戳的最快方式呢?
获取10位时间戳性能对比
验证代码如下:
const performance = require('perf_hooks').performance
let s, e, interval = 10000000
console.log(`\n\n获取${interval}次10位时间戳速度对比:====================================`)
s = process.uptime()
for (let i = 0; i < interval; i++) Math.floor((performance.timeOrigin + performance.now()) / 1000)
e = process.uptime()
console.log('Math.floor((performance.timeOrigin + performance.now()) / 1000)', Math.floor((performance.timeOrigin + performance.now()) / 1000), e - s)
s = process.uptime()
for (let i = 0; i < interval; i++) Math.floor(Date.now() / 1000)
e = process.uptime()
console.log('Math.floor(Date.now()/1000)', Math.floor(Date.now() / 1000), e - s)
s = process.uptime()
for (let i = 0; i < interval; i++) Math.floor(new Date().getTime() / 1000)
e = process.uptime()
console.log('Math.floor(new Date().getTime()/1000)', Math.floor(new Date().getTime() / 1000), e - s)
s = process.uptime()
for (let i = 0; i < interval; i++) Date.parse(new Date()) / 1000
e = process.uptime()
console.log('Date.parse(new Date())/1000', Date.parse(new Date()) / 1000, e - s)
结果如下:
获取10000000次10位时间戳速度对比:====================================
Math.floor((performance.timeOrigin + performance.now()) / 1000) 1596591749 0.476
Math.floor(Date.now()/1000) 1596591750 0.889
Math.floor(new Date().getTime()/1000) 1596591751 1.6669999999999998
Date.parse(new Date())/1000 1596591774 22.153
所以还是performance
完美胜出!
是否还有更快的方式?
经过上面测试,在我的目前的认知范围内(小学生阶段),也就是performance
获取13位时间戳的性能最高了。那这种方式是否还有优化的可能呢?
- 我们知道
performance.timeOrigin
是一个精确到微秒的变量,在系统运行的时候就直接将当前的时间赋值给了它,所以获取它应该是没有什么可以优化的空间了。 - 经过测试,通过将数字转换为字符或字符串后再取前几位的方式,不论是空间复杂度还是时间复杂度来说和直接的数学运算来比相差很大,慢了好多倍,所以,优化的重点在触发计算和取整这块了。
- 那么优化的空间可能就藏在除法计算和取整这个环节了。经过一番对除法取整的探索,结果如下:
exact division
-
通过Math库取整及速度对比
可以看到,效率最高的还是Math.floor()
这个方法。这里就会联想到Math.trunc()
,它们两个之间的性能在计算时间戳这块的对比如何呢,代码如下:
const performance = require('perf_hooks').performance;
let s, e, a, interval = 1000000000
console.log(`\n\n执行${interval}次速度对比:====================================`)
a = (performance.timeOrigin + performance.now()) / 1000
s = process.uptime()
for (let i = 0; i < interval; i++) Math.floor(a)
e = process.uptime()
console.log('Math.floor', a, Math.floor(a), e - s)
s = process.uptime()
for (let i = 0; i < interval; i++) Math.trunc(a)
e = process.uptime()
console.log('Math.trunc', a, Math.trunc(a), e - s)
对比结果如下,两个性能差距相差不大,每次测试结果都大致相同:
执行1000000000次速度对比:====================================
Math.floor 1596596494.6187212 1596596494 0.984
Math.trunc 1596596494.6187212 1596596494 0.984
执行10000000000次速度对比:====================================
Math.floor 1596596596.3742654 1596596596 13.716
Math.trunc 1596596596.3742654 1596596596 14.643
-
通过位运算进行取整
更多位运算相关请移步《JS中的位运算》了解更多
通过位运算X|0
,~~X
,X^0
,X>>0
,X<<0
都可以实现小数的取整
单竖杠“|”就是位运算中的按位或运算:
比如:3|4,就是0011 | 0100=0111=4+2+1=7
再如:1596596596.3742654 | 0,首先我们需要知道1596596596.3742654的二进制存储格式了。
JavaScript 只有一种数字类型 ( Number )
IEEE标准中float的存储规则
JavaScript采用 IEEE 754 标准双精度浮点(double64),64位中有1位符号位,11位存储指数,52位存储浮点数的有效数字
有时候小数在二进制中表示是无限的,所以从53位开始就会舍入(舍入规则是0舍1入),这样就造成了“浮点精度问题”(由于舍入规则有时大点,有时小点)
IEEE标准中double的存储规则
更多详细介绍,请参看传送门
JS中小数的存储方式
通过上面的了解,我们将上面的1596596596.3742654.toString(2)
转为二进制字符串表示如下:
1011111001010100010000101110100.0101111111001111110111
但实际在内存中的存储如下:
- 首先将整数部分
1596596596
转为二进制:1011111001010100010000101110100
- 将小数部分转为二进制:
0.010111111100111111011011011101010000011000111100010111
- 所以其二进制拼接后为:
1011111001010100010000101110100.010111111100111111011011011101010000011000111100010111
,但显然位数超出了64位的限制,而且小数点也不可能存储的为小数点(只有0和1啊) - 所以将小数点左移30位后转为科学计数法:
1.011111001010100010000101110100010111111100111111011011011101010000011000111100010111 * 2^30
- 正数,符号位为0,我们在最高位符号位中填0
- 指数部分,通过左移得到的,指数为正,因此62位填1,然后将指数
30-1=29
,二进制为101001,在左边添0,所以61~52位凑够了10位,因此指数部分为100 0010 1001
- 至于尾数部分,直接将科学计数法后小数点后面的数扔进去即可(因为超出52位长度,所以更多的位数会舍去,最后一位会0舍1入),所以尾数部分为:
0111110010101000100001011101000101111111001111110111
- 至此,这个浮点数的二进制就存储为:
0100 0010 1001 0111 1100 1010 1000 1000 0101 1101 0001 0111 1111 0011 1111 0111
,转为16进制为:0x4297CA885D17F3F7
番外篇:JS中的精度丢失
说到这里就不得不简单提一下数字精度丢失的问题。上面也知道,JS中所有的数字都是用double方式进行存储的,所以必然会存在精度丢失问题。
以下转自文章:JavaScript数字精度丢失问题总结
此时只能模仿十进制进行四舍五入了,但是二进制只有 0 和 1 两个,于是变为 0 舍 1 入。这即是计算机中部分浮点数运算时出现误差,丢失精度的根本原因。
大整数的精度丢失和浮点数本质上是一样的,尾数位最大是 52 位,因此 JS 中能精准表示的最大整数是 Math.pow(2, 53)
,十进制即 9007199254740992
大于9007199254740992
的可能会丢失精度:
9007199254740992 >> 10000000000000...000 ``// 共计 53 个 0
9007199254740992 + 1 >> 10000000000000...001 ``// 中间 52 个 0
9007199254740992 + 2 >> 10000000000000...010 ``// 中间 51 个 0
实际上
9007199254740992 + 1 ``// 丢失
9007199254740992 + 2 ``// 未丢失
9007199254740992 + 3 ``// 丢失
9007199254740992 + 4 ``// 未丢失
以上,可以知道看似有穷的数字, 在计算机的二进制表示里却是无穷的,由于存储位数限制因此存在“舍去”,精度丢失就发生了。
想了解更深入的分析可以看这篇论文(你品!你细品!):What Every Computer Scientist Should Know About Floating-Point Arithmetic
关于精度和范围的内容可查看【JS的数值精度和数值范围】
番外篇2:JS中的位运算数据异常
位运算只对整数起作用,如果一个运算子不是整数,会自动转为整数后再运行。虽然在 JavaScript 内部,数值都是以64位浮点数的形式储存,但是做位运算的时候,是以32位带符号的整数进行运算的,并且返回值也是一个32位带符号的整数。
ECMAScript 中,所有整数字面量默认都是有符号整数,这意味着什么呢?有符号整数使用 31 位表示整数的数值,用第 32 位表示整数的符号,0 表示正数,1 表示负数。数值范围从
-2147483648 到 2147483647
。
这也就是为什么对于整数部位为10位的时间戳,通过位运算可以进行取整(因为目前时间戳159xxxxxxx<2147483647),不存在时间戳超过范围的问题。但是对于13位时间戳,如1596615447123>2147483647
,此时再通过位运算操作的时候就会导致异常,如:
let t = 1596615447015.007
console.log(Math.trunc(t), Math.trunc(t / 1000)) // 1596615447015 1596615447
console.log(t / 1000 | 0) // 1596615447
console.log(t | 0) // -1112387097
这主要是因为在进行位运算之前,JS会先将64bit的浮点数1596615447015.01
转为32bit的有符号整型后进行运算的,这个转换过程如下:
- 首先
1596615447015.333
的二进制表示为10111001110111101101100100101000111100111.0101010101
,其在内存中的存储结构如下:- 正数,最高位符号位
0
- 科学计数法小数点左移,指数位最高位为
1
- 小数点左移40位,则剩余指数部分为
40-1=39
的10位二进制00 0010 0111
- 所以前12位为
0100 0010 0111
- 正数,最高位符号位
- 剩余52位从小数点后开始取52位(不足52位在最后补0,超过则最后一位0舍1入)为
0111001110111101101100100101000111100111010101010100
- 所以
1596615447015.333
的二进制存储表示为:0100 0010 0111 0111 0011 1011 1101 1011 0010 0101 0001 1110 0111 0101 0101 0100
,转为16进制表示为:0x42773BDB251E7554
- 开始将其转为32bit的int类型,首先根据指数位
100 0010 0111
可知,小数点右移39+1=40位,剩余小数位数舍掉,则52位尾数部分得到的是73BDB251E7
,即二进制表示为0111 0011 1011 1101 1011 0010 0101 0001 1110 0111
- 截取上面二进制的后32位得到:
1011 1101 1011 0010 0101 0001 1110 0111
,系统会将这32位数当作转换后的int类型,由于最高位为1
,即这是一个负数 - 对于系统来说,如果是负数,则用这个负数的补码表示,即这个负数绝对值的二进制按位取反,然后最后一位执行不进位+1的来的,所以对于上面这个二进制,将其转为10进制的过程如下:
- 最高位符号位为1,表示负数
- 既然是负数,最后一位不退位-1,得到:
011 1101 1011 0010 0101 0001 1110 0110
- 取补码:
100 0010 0100 1101 1010 1110 0001 1001
- 表示为十进制:
-1112387097
- 至此,就可以解释为什么
1596615447015.333 | 0 = -1112387097
了。
为了验证上述过程,我们再举一个例子:1590015447015.123 >> 0 = 877547495
-
1590015447015.123
的二进制表示为:10111001000110100010011100100111111100111.000111111
- 舍去其小数部分后,从后往前取32位为:
00110100010011100100111111100111
- 最高位为0,正数,直接转为10进制为:
877547495
将将将将!没错的吧!所以JS的这个坑还真是。。。 让人无语
回归正题
经过上面的一番折腾,我们知道了超过10位的时间戳(实际上是大于2^32的数),通过位运算都会导致数据异常,所以对于通过位运算对时间戳取整,我们还是需要先将其改为10位时间戳后再取整才可以,废话不多说,直接上代码:
const performance = require('perf_hooks').performance;
let s, e, interval = 1000000000
console.log(`\n\n执行${interval}次速度对比:====================================`)
let a = (performance.timeOrigin + performance.now()) / 1000
s = process.uptime()
for (let i = 0; i < interval; i++) Math.floor(a)
e = process.uptime()
console.log('Math.floor', a, Math.floor(a), e - s)
s = process.uptime()
for (let i = 0; i < interval; i++) Math.trunc(a)
e = process.uptime()
console.log('Math.trunc', a, Math.trunc(a), e - s)
s = process.uptime()
for (let i = 0; i < interval; i++) a >> 0
e = process.uptime()
console.log('X>>0', a, a >> 0, e - s)
s = process.uptime()
for (let i = 0; i < interval; i++) a << 0
e = process.uptime()
console.log('X<<0', a, a << 0, e - s)
s = process.uptime()
for (let i = 0; i < interval; i++) a | 0
e = process.uptime()
console.log('X|0', a, a | 0, e - s)
s = process.uptime()
for (let i = 0; i < interval; i++) ~~a
e = process.uptime()
console.log('~~X', a, ~~a, e - s)
得到的结果:
执行1000000000次速度对比:====================================
Math.floor 1596625611.8681817 1596625611 0.9910000000000001
Math.trunc 1596625611.8681817 1596625611 0.9850000000000001
X>>0 1596625611.8681817 1596625611 0.649
X<<0 1596625611.8681817 1596625611 0.6599999999999997
X|0 1596625611.8681817 1596625611 0.6659999999999995
~~X 1596625611.8681817 1596625611 0.6550000000000002
是不是很惊喜!!!位运算的效率果然会领先于Math
库
至此,我们一直找到了最快获取时间戳和最快取整的两个手段了,分别是通过performance
库和>>等
位运算实现。那是不是还有优化的空间呢?再回过头来看一下我们的业务代码:
const performance = require('perf_hooks').performance;
let s, e, interval = 100000000
console.log(`\n\n执行${interval}次速度对比:====================================`)
s = process.uptime()
for (let i = 0; i < interval; i++) getTimestamp1()
e = process.uptime()
console.log('getTimestamp1', getTimestamp1(), e - s)
s = process.uptime()
for (let i = 0; i < interval; i++) getTimestamp2()
e = process.uptime()
console.log('getTimestamp2', getTimestamp2(), e - s)
s = process.uptime()
for (let i = 0; i < interval; i++) getTimestamp3()
e = process.uptime()
console.log('getTimestamp3', getTimestamp3(), e - s)
function getTimestamp1() {
return (performance.timeOrigin + performance.now()) / 1000 >> 0
}
function getTimestamp2() {
return (performance.timeOrigin / 1000 >> 0) + (performance.now() / 1000 >> 0)
}
function getTimestamp3() {
return Math.trunc((performance.timeOrigin + performance.now()) / 1000)
}
运行结果:
执行100000000次速度对比:====================================
getTimestamp1 1596628296 4.924
getTimestamp2 1596628301 5.109
getTimestamp3 1596628306 5.022
-
归纳总结
- 对于获取系统时间来说,通过
performance
实现性能最高 - 对于取整运算来说,
位运算
的效率最高 - 尽可能减少
除法
的使用,因为它效率最慢
所以,获取系统10位时间戳最快的方式就是下面这一句:
const performance = require('perf_hooks').performance;
(performance.timeOrigin + performance.now()) / 1000 >> 0