【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位时间戳的性能最高了。那这种方式是否还有优化的可能呢?

  1. 我们知道performance.timeOrigin是一个精确到微秒的变量,在系统运行的时候就直接将当前的时间赋值给了它,所以获取它应该是没有什么可以优化的空间了。
  2. 经过测试,通过将数字转换为字符或字符串后再取前几位的方式,不论是空间复杂度还是时间复杂度来说和直接的数学运算来比相差很大,慢了好多倍,所以,优化的重点在触发计算和取整这块了。
  3. 那么优化的空间可能就藏在除法计算和取整这个环节了。经过一番对除法取整的探索,结果如下
    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 )
JavaScript采用 IEEE 754 标准双精度浮点(double64),64位中有1位符号位,11位存储指数,52位存储浮点数的有效数字
有时候小数在二进制中表示是无限的,所以从53位开始就会舍入(舍入规则是0舍1入),这样就造成了“浮点精度问题”(由于舍入规则有时大点,有时小点)

IEEE标准中float的存储规则

IEEE标准中double的存储规则

更多详细介绍,请参看传送门

JS中小数的存储方式

通过上面的了解,我们将上面的1596596596.3742654.toString(2)转为二进制字符串表示如下:
1011111001010100010000101110100.0101111111001111110111
但实际在内存中的存储如下:

  1. 首先将整数部分1596596596转为二进制:1011111001010100010000101110100
  2. 将小数部分转为二进制:0.010111111100111111011011011101010000011000111100010111
  3. 所以其二进制拼接后为:1011111001010100010000101110100.010111111100111111011011011101010000011000111100010111,但显然位数超出了64位的限制,而且小数点也不可能存储的为小数点(只有0和1啊)
  4. 所以将小数点左移30位后转为科学计数法:1.011111001010100010000101110100010111111100111111011011011101010000011000111100010111 * 2^30
  5. 正数,符号位为0,我们在最高位符号位中填0
  6. 指数部分,通过左移得到的,指数为正,因此62位填1,然后将指数30-1=29,二进制为101001,在左边添0,所以61~52位凑够了10位,因此指数部分为100 0010 1001
  7. 至于尾数部分,直接将科学计数法后小数点后面的数扔进去即可(因为超出52位长度,所以更多的位数会舍去,最后一位会0舍1入),所以尾数部分为:0111110010101000100001011101000101111111001111110111
  8. 至此,这个浮点数的二进制就存储为: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的有符号整型后进行运算的,这个转换过程如下:

32bit整型存储结构
  1. 首先1596615447015.333的二进制表示为10111001110111101101100100101000111100111.0101010101,其在内存中的存储结构如下:
    1. 正数,最高位符号位0
    2. 科学计数法小数点左移,指数位最高位为1
    3. 小数点左移40位,则剩余指数部分为40-1=39的10位二进制00 0010 0111
    4. 所以前12位为0100 0010 0111
  2. 剩余52位从小数点后开始取52位(不足52位在最后补0,超过则最后一位0舍1入)为0111001110111101101100100101000111100111010101010100
  3. 所以1596615447015.333的二进制存储表示为:0100 0010 0111 0111 0011 1011 1101 1011 0010 0101 0001 1110 0111 0101 0101 0100,转为16进制表示为:0x42773BDB251E7554
  4. 开始将其转为32bit的int类型,首先根据指数位100 0010 0111可知,小数点右移39+1=40位,剩余小数位数舍掉,则52位尾数部分得到的是73BDB251E7,即二进制表示为0111 0011 1011 1101 1011 0010 0101 0001 1110 0111
  5. 截取上面二进制的后32位得到:1011 1101 1011 0010 0101 0001 1110 0111,系统会将这32位数当作转换后的int类型,由于最高位为1,即这是一个负数
  6. 对于系统来说,如果是负数,则用这个负数的补码表示,即这个负数绝对值的二进制按位取反,然后最后一位执行不进位+1的来的,所以对于上面这个二进制,将其转为10进制的过程如下:
    1. 最高位符号位为1,表示负数
    2. 既然是负数,最后一位不退位-1,得到:011 1101 1011 0010 0101 0001 1110 0110
    3. 取补码:100 0010 0100 1101 1010 1110 0001 1001
    4. 表示为十进制:-1112387097
  7. 至此,就可以解释为什么1596615447015.333 | 0 = -1112387097了。

为了验证上述过程,我们再举一个例子:1590015447015.123 >> 0 = 877547495

  1. 1590015447015.123的二进制表示为:10111001000110100010011100100111111100111.000111111
  2. 舍去其小数部分后,从后往前取32位为:00110100010011100100111111100111
  3. 最高位为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
  • 归纳总结

  1. 对于获取系统时间来说,通过performance实现性能最高
  2. 对于取整运算来说,位运算的效率最高
  3. 尽可能减少除法的使用,因为它效率最慢

所以,获取系统10位时间戳最快的方式就是下面这一句:

const performance = require('perf_hooks').performance;
(performance.timeOrigin + performance.now()) / 1000 >> 0
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343