【JS時間戳】獲取時間戳的最快方式探究

TAG

nodejs,nodejs時間戳,js時間戳,timestamp,date.now,performance.now,時間戳,小數(shù)取整,位運算,精度丟失,數(shù)字的存儲方式,小數(shù)的二進制存儲,位運算的限制

獲取13位時間戳方法及性能簡單對比

以前獲取時間戳沒什么太認真過,今天突然突發(fā)奇想,哪種方式獲取時間戳最快呢?特別是常用的10位時間戳。然后了解到獲取時間戳的方式有很多種,比如網(wǎng)上常用的下面幾種方式(除了第一種):

// 下列速度依次遞減
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)

結(jié)果如下:

獲取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之外,其他幾種方式網(wǎng)上的比對一大堆,大家就自行了解啦。起初我也是以為Date.now()是最快的,但是當帶著好奇去了解的時候,突然發(fā)現(xiàn)了這個performance,然后一測試發(fā)現(xiàn)了新大陸!關(guān)于performance的介紹,請看我另外一篇轉(zhuǎn)載的文章:《解讀 Nodejs 性能 API:Performance Timing》

這幾種方式的對比這里就不再贅述了,由上往下速度遞減,performance完勝,更多詳細對比網(wǎng)上一大堆。但是如果說獲取時間戳,基本都是精確到毫秒的13位時間戳(除了Date.parse)。但是日常開發(fā)中很多時候用到的是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)

結(jié)果如下:

獲取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完美勝出!

是否還有更快的方式?

經(jīng)過上面測試,在我的目前的認知范圍內(nèi)(小學生階段),也就是performance獲取13位時間戳的性能最高了。那這種方式是否還有優(yōu)化的可能呢?

  1. 我們知道performance.timeOrigin是一個精確到微秒的變量,在系統(tǒng)運行的時候就直接將當前的時間賦值給了它,所以獲取它應(yīng)該是沒有什么可以優(yōu)化的空間了。
  2. 經(jīng)過測試,通過將數(shù)字轉(zhuǎn)換為字符或字符串后再取前幾位的方式,不論是空間復(fù)雜度還是時間復(fù)雜度來說和直接的數(shù)學運算來比相差很大,慢了好多倍,所以,優(yōu)化的重點在觸發(fā)計算和取整這塊了。
  3. 那么優(yōu)化的空間可能就藏在除法計算和取整這個環(huán)節(jié)了。經(jīng)過一番對除法取整的探索,結(jié)果如下
    exact division
  • 通過Math庫取整及速度對比

可以看到,效率最高的還是Math.floor()這個方法。這里就會聯(lián)想到Math.trunc(),它們兩個之間的性能在計算時間戳這塊的對比如何呢,代碼如下:

const performance = require('perf_hooks').performance;
let s, e, a, interval = 1000000000
console.log(`\n\n執(zhí)行${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)

對比結(jié)果如下,兩個性能差距相差不大,每次測試結(jié)果都大致相同:

執(zhí)行1000000000次速度對比:====================================
Math.floor 1596596494.6187212 1596596494 0.984
Math.trunc 1596596494.6187212 1596596494 0.984

執(zhí)行10000000000次速度對比:====================================
Math.floor 1596596596.3742654 1596596596 13.716
Math.trunc 1596596596.3742654 1596596596 14.643
  • 通過位運算進行取整

更多位運算相關(guān)請移步《JS中的位運算》了解更多
通過位運算X|0,~~X,X^0,X>>0,X<<0都可以實現(xiàn)小數(shù)的取整

單豎杠“|”就是位運算中的按位或運算:
比如:3|4,就是0011 | 0100=0111=4+2+1=7
再如:1596596596.3742654 | 0,首先我們需要知道1596596596.3742654的二進制存儲格式了。

JavaScript 只有一種數(shù)字類型 ( Number )
JavaScript采用 IEEE 754 標準雙精度浮點(double64),64位中有1位符號位,11位存儲指數(shù),52位存儲浮點數(shù)的有效數(shù)字
有時候小數(shù)在二進制中表示是無限的,所以從53位開始就會舍入(舍入規(guī)則是0舍1入),這樣就造成了“浮點精度問題”(由于舍入規(guī)則有時大點,有時小點)

IEEE標準中float的存儲規(guī)則

IEEE標準中double的存儲規(guī)則

更多詳細介紹,請參看傳送門

JS中小數(shù)的存儲方式

通過上面的了解,我們將上面的1596596596.3742654.toString(2)轉(zhuǎn)為二進制字符串表示如下:
1011111001010100010000101110100.0101111111001111110111
但實際在內(nèi)存中的存儲如下:

  1. 首先將整數(shù)部分1596596596轉(zhuǎn)為二進制:1011111001010100010000101110100
  2. 將小數(shù)部分轉(zhuǎn)為二進制:0.010111111100111111011011011101010000011000111100010111
  3. 所以其二進制拼接后為:1011111001010100010000101110100.010111111100111111011011011101010000011000111100010111,但顯然位數(shù)超出了64位的限制,而且小數(shù)點也不可能存儲的為小數(shù)點(只有0和1啊)
  4. 所以將小數(shù)點左移30位后轉(zhuǎn)為科學計數(shù)法:1.011111001010100010000101110100010111111100111111011011011101010000011000111100010111 * 2^30
  5. 正數(shù),符號位為0,我們在最高位符號位中填0
  6. 指數(shù)部分,通過左移得到的,指數(shù)為正,因此62位填1,然后將指數(shù)30-1=29,二進制為101001,在左邊添0,所以61~52位湊夠了10位,因此指數(shù)部分為100 0010 1001
  7. 至于尾數(shù)部分,直接將科學計數(shù)法后小數(shù)點后面的數(shù)扔進去即可(因為超出52位長度,所以更多的位數(shù)會舍去,最后一位會0舍1入),所以尾數(shù)部分為:0111110010101000100001011101000101111111001111110111
  8. 至此,這個浮點數(shù)的二進制就存儲為:0100 0010 1001 0111 1100 1010 1000 1000 0101 1101 0001 0111 1111 0011 1111 0111,轉(zhuǎn)為16進制為:0x4297CA885D17F3F7
番外篇:JS中的精度丟失

說到這里就不得不簡單提一下數(shù)字精度丟失的問題。上面也知道,JS中所有的數(shù)字都是用double方式進行存儲的,所以必然會存在精度丟失問題。

以下轉(zhuǎn)自文章:JavaScript數(shù)字精度丟失問題總結(jié)

此時只能模仿十進制進行四舍五入了,但是二進制只有 0 和 1 兩個,于是變?yōu)?0 舍 1 入。這即是計算機中部分浮點數(shù)運算時出現(xiàn)誤差,丟失精度的根本原因。

大整數(shù)的精度丟失和浮點數(shù)本質(zhì)上是一樣的,尾數(shù)位最大是 52 位,因此 JS 中能精準表示的最大整數(shù)是 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 ``// 未丟失

以上,可以知道看似有窮的數(shù)字, 在計算機的二進制表示里卻是無窮的,由于存儲位數(shù)限制因此存在“舍去”,精度丟失就發(fā)生了。

想了解更深入的分析可以看這篇論文(你品!你細品!):What Every Computer Scientist Should Know About Floating-Point Arithmetic
關(guān)于精度和范圍的內(nèi)容可查看【JS的數(shù)值精度和數(shù)值范圍】

番外篇2:JS中的位運算數(shù)據(jù)異常

位運算只對整數(shù)起作用,如果一個運算子不是整數(shù),會自動轉(zhuǎn)為整數(shù)后再運行。雖然在 JavaScript 內(nèi)部,數(shù)值都是以64位浮點數(shù)的形式儲存,但是做位運算的時候,是以32位帶符號的整數(shù)進行運算的,并且返回值也是一個32位帶符號的整數(shù)。

ECMAScript 中,所有整數(shù)字面量默認都是有符號整數(shù),這意味著什么呢?有符號整數(shù)使用 31 位表示整數(shù)的數(shù)值,用第 32 位表示整數(shù)的符號,0 表示正數(shù),1 表示負數(shù)。數(shù)值范圍從-2147483648 到 2147483647。

這也就是為什么對于整數(shù)部位為10位的時間戳,通過位運算可以進行取整(因為目前時間戳159xxxxxxx<2147483647),不存在時間戳超過范圍的問題。但是對于13位時間戳,如1596615447123>2147483647,此時再通過位運算操作的時候就會導(dǎo)致異常,如:

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的浮點數(shù)1596615447015.01轉(zhuǎn)為32bit的有符號整型后進行運算的,這個轉(zhuǎn)換過程如下:

32bit整型存儲結(jié)構(gòu)
  1. 首先1596615447015.333的二進制表示為10111001110111101101100100101000111100111.0101010101,其在內(nèi)存中的存儲結(jié)構(gòu)如下:
    1. 正數(shù),最高位符號位0
    2. 科學計數(shù)法小數(shù)點左移,指數(shù)位最高位為1
    3. 小數(shù)點左移40位,則剩余指數(shù)部分為40-1=39的10位二進制00 0010 0111
    4. 所以前12位為0100 0010 0111
  2. 剩余52位從小數(shù)點后開始取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,轉(zhuǎn)為16進制表示為:0x42773BDB251E7554
  4. 開始將其轉(zhuǎn)為32bit的int類型,首先根據(jù)指數(shù)位100 0010 0111可知,小數(shù)點右移39+1=40位,剩余小數(shù)位數(shù)舍掉,則52位尾數(shù)部分得到的是73BDB251E7,即二進制表示為0111 0011 1011 1101 1011 0010 0101 0001 1110 0111
  5. 截取上面二進制的后32位得到:1011 1101 1011 0010 0101 0001 1110 0111,系統(tǒng)會將這32位數(shù)當作轉(zhuǎn)換后的int類型,由于最高位為1,即這是一個負數(shù)
  6. 對于系統(tǒng)來說,如果是負數(shù),則用這個負數(shù)的補碼表示,即這個負數(shù)絕對值的二進制按位取反,然后最后一位執(zhí)行不進位+1的來的,所以對于上面這個二進制,將其轉(zhuǎn)為10進制的過程如下:
    1. 最高位符號位為1,表示負數(shù)
    2. 既然是負數(shù),最后一位不退位-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. 舍去其小數(shù)部分后,從后往前取32位為:00110100010011100100111111100111
  3. 最高位為0,正數(shù),直接轉(zhuǎn)為10進制為:877547495

將將將將!沒錯的吧!所以JS的這個坑還真是。。。 讓人無語

回歸正題

經(jīng)過上面的一番折騰,我們知道了超過10位的時間戳(實際上是大于2^32的數(shù)),通過位運算都會導(dǎo)致數(shù)據(jù)異常,所以對于通過位運算對時間戳取整,我們還是需要先將其改為10位時間戳后再取整才可以,廢話不多說,直接上代碼:

const performance = require('perf_hooks').performance;

let s, e, interval = 1000000000

console.log(`\n\n執(zhí)行${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)

得到的結(jié)果:

執(zhí)行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

是不是很驚喜?。?!位運算的效率果然會領(lǐng)先于Math

至此,我們一直找到了最快獲取時間戳和最快取整的兩個手段了,分別是通過performance庫和>>等位運算實現(xiàn)。那是不是還有優(yōu)化的空間呢?再回過頭來看一下我們的業(yè)務(wù)代碼:

const performance = require('perf_hooks').performance;

let s, e, interval = 100000000

console.log(`\n\n執(zhí)行${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)
}

運行結(jié)果:

執(zhí)行100000000次速度對比:====================================
getTimestamp1 1596628296 4.924
getTimestamp2 1596628301 5.109
getTimestamp3 1596628306 5.022
  • 歸納總結(jié)

  1. 對于獲取系統(tǒng)時間來說,通過performance實現(xiàn)性能最高
  2. 對于取整運算來說,位運算的效率最高
  3. 盡可能減少除法的使用,因為它效率最慢

所以,獲取系統(tǒng)10位時間戳最快的方式就是下面這一句:

const performance = require('perf_hooks').performance;
(performance.timeOrigin + performance.now()) / 1000 >> 0
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容