TAG
nodejs,nodejs時間戳,js時間戳,timestamp,date.now,performance.now,時間戳,小數(shù)取整,位運算,精度丟失,數(shù)字的存儲方式,小數(shù)的二進(jìn)制存儲,位運算的限制
獲取13位時間戳方法及性能簡單對比
以前獲取時間戳沒什么太認(rèn)真過鳞贷,今天突然突發(fā)奇想,哪種方式獲取時間戳最快呢?特別是常用的10位時間戳。然后了解到獲取時間戳的方式有很多種,比如網(wǎng)上常用的下面幾種方式(除了第一種):
// 下列速度依次遞減
performance.timeOrigin + performance.now()
Date.now()
new Date().getTime()
new Date().valueOf()
Date.parse(new Date())
// 下列兩個方法獲取時間差等
process.uptime()
process.hrtime()
通過如下代碼進(jìn)行驗證:
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()
是最快的拐揭,但是當(dāng)帶著好奇去了解的時候,突然發(fā)現(xiàn)了這個performance
奕塑,然后一測試發(fā)現(xiàn)了新大陸堂污!關(guān)于performance
的介紹,請看我另外一篇轉(zhuǎn)載的文章:《解讀 Nodejs 性能 API:Performance Timing》
這幾種方式的對比這里就不再贅述了龄砰,由上往下速度遞減盟猖,performance完勝,更多詳細(xì)對比網(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)過上面測試价说,在我的目前的認(rèn)知范圍內(nèi)(小學(xué)生階段),也就是performance
獲取13位時間戳的性能最高了风秤。那這種方式是否還有優(yōu)化的可能呢鳖目?
- 我們知道
performance.timeOrigin
是一個精確到微秒的變量,在系統(tǒng)運行的時候就直接將當(dāng)前的時間賦值給了它缤弦,所以獲取它應(yīng)該是沒有什么可以優(yōu)化的空間了领迈。 - 經(jīng)過測試,通過將數(shù)字轉(zhuǎn)換為字符或字符串后再取前幾位的方式,不論是空間復(fù)雜度還是時間復(fù)雜度來說和直接的數(shù)學(xué)運算來比相差很大狸捅,慢了好多倍衷蜓,所以,優(yōu)化的重點在觸發(fā)計算和取整這塊了尘喝。
- 那么優(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
-
通過位運算進(jìn)行取整
更多位運算相關(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的二進(jìn)制存儲格式了。
JavaScript 只有一種數(shù)字類型 ( Number )
JavaScript采用 IEEE 754 標(biāo)準(zhǔn)雙精度浮點(double64)踢匣,64位中有1位符號位置森,11位存儲指數(shù),52位存儲浮點數(shù)的有效數(shù)字
有時候小數(shù)在二進(jìn)制中表示是無限的符糊,所以從53位開始就會舍入(舍入規(guī)則是0舍1入),這樣就造成了“浮點精度問題”(由于舍入規(guī)則有時大點呛凶,有時小點)
IEEE標(biāo)準(zhǔn)中float的存儲規(guī)則
IEEE標(biāo)準(zhǔn)中double的存儲規(guī)則
更多詳細(xì)介紹男娄,請參看傳送門
JS中小數(shù)的存儲方式
通過上面的了解,我們將上面的1596596596.3742654.toString(2)
轉(zhuǎn)為二進(jìn)制字符串表示如下:
1011111001010100010000101110100.0101111111001111110111
但實際在內(nèi)存中的存儲如下:
- 首先將整數(shù)部分
1596596596
轉(zhuǎn)為二進(jìn)制:1011111001010100010000101110100
- 將小數(shù)部分轉(zhuǎn)為二進(jìn)制:
0.010111111100111111011011011101010000011000111100010111
- 所以其二進(jìn)制拼接后為:
1011111001010100010000101110100.010111111100111111011011011101010000011000111100010111
漾稀,但顯然位數(shù)超出了64位的限制模闲,而且小數(shù)點也不可能存儲的為小數(shù)點(只有0和1啊) - 所以將小數(shù)點左移30位后轉(zhuǎn)為科學(xué)計數(shù)法:
1.011111001010100010000101110100010111111100111111011011011101010000011000111100010111 * 2^30
- 正數(shù)崭捍,符號位為0尸折,我們在最高位符號位中填0
- 指數(shù)部分,通過左移得到的殷蛇,指數(shù)為正实夹,因此62位填1,然后將指數(shù)
30-1=29
粒梦,二進(jìn)制為101001亮航,在左邊添0,所以61~52位湊夠了10位匀们,因此指數(shù)部分為100 0010 1001
- 至于尾數(shù)部分缴淋,直接將科學(xué)計數(shù)法后小數(shù)點后面的數(shù)扔進(jìn)去即可(因為超出52位長度,所以更多的位數(shù)會舍去,最后一位會0舍1入)重抖,所以尾數(shù)部分為:
0111110010101000100001011101000101111111001111110111
- 至此露氮,這個浮點數(shù)的二進(jìn)制就存儲為:
0100 0010 1001 0111 1100 1010 1000 1000 0101 1101 0001 0111 1111 0011 1111 0111
,轉(zhuǎn)為16進(jìn)制為:0x4297CA885D17F3F7
番外篇:JS中的精度丟失
說到這里就不得不簡單提一下數(shù)字精度丟失的問題钟沛。上面也知道畔规,JS中所有的數(shù)字都是用double方式進(jìn)行存儲的,所以必然會存在精度丟失問題讹剔。
以下轉(zhuǎn)自文章:JavaScript數(shù)字精度丟失問題總結(jié)
此時只能模仿十進(jìn)制進(jìn)行四舍五入了油讯,但是二進(jìn)制只有 0 和 1 兩個,于是變?yōu)?0 舍 1 入延欠。這即是計算機中部分浮點數(shù)運算時出現(xiàn)誤差陌兑,丟失精度的根本原因。
大整數(shù)的精度丟失和浮點數(shù)本質(zhì)上是一樣的由捎,尾數(shù)位最大是 52 位兔综,因此 JS 中能精準(zhǔn)表示的最大整數(shù)是 Math.pow(2, 53)
,十進(jìn)制即 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ù)字, 在計算機的二進(jìn)制表示里卻是無窮的软驰,由于存儲位數(shù)限制因此存在“舍去”,精度丟失就發(fā)生了心肪。
想了解更深入的分析可以看這篇論文(你品锭亏!你細(xì)品!):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ù)進(jìn)行運算的伐坏,并且返回值也是一個32位帶符號的整數(shù)怔匣。
ECMAScript 中,所有整數(shù)字面量默認(rèn)都是有符號整數(shù)桦沉,這意味著什么呢每瞒?有符號整數(shù)使用 31 位表示整數(shù)的數(shù)值,用第 32 位表示整數(shù)的符號纯露,0 表示正數(shù)独泞,1 表示負(fù)數(shù)。數(shù)值范圍從
-2147483648 到 2147483647
苔埋。
這也就是為什么對于整數(shù)部位為10位的時間戳懦砂,通過位運算可以進(jìn)行取整(因為目前時間戳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
這主要是因為在進(jìn)行位運算之前痊班,JS會先將64bit的浮點數(shù)1596615447015.01
轉(zhuǎn)為32bit的有符號整型后進(jìn)行運算的,這個轉(zhuǎn)換過程如下:
- 首先
1596615447015.333
的二進(jìn)制表示為10111001110111101101100100101000111100111.0101010101
,其在內(nèi)存中的存儲結(jié)構(gòu)如下:- 正數(shù)霜幼,最高位符號位
0
- 科學(xué)計數(shù)法小數(shù)點左移屠升,指數(shù)位最高位為
1
- 小數(shù)點左移40位潮改,則剩余指數(shù)部分為
40-1=39
的10位二進(jìn)制00 0010 0111
- 所以前12位為
0100 0010 0111
- 正數(shù)霜幼,最高位符號位
- 剩余52位從小數(shù)點后開始取52位(不足52位在最后補0,超過則最后一位0舍1入)為
0111001110111101101100100101000111100111010101010100
- 所以
1596615447015.333
的二進(jìn)制存儲表示為:0100 0010 0111 0111 0011 1011 1101 1011 0010 0101 0001 1110 0111 0101 0101 0100
腹暖,轉(zhuǎn)為16進(jìn)制表示為:0x42773BDB251E7554
- 開始將其轉(zhuǎn)為32bit的int類型汇在,首先根據(jù)指數(shù)位
100 0010 0111
可知,小數(shù)點右移39+1=40位脏答,剩余小數(shù)位數(shù)舍掉糕殉,則52位尾數(shù)部分得到的是73BDB251E7
,即二進(jìn)制表示為0111 0011 1011 1101 1011 0010 0101 0001 1110 0111
- 截取上面二進(jìn)制的后32位得到:
1011 1101 1011 0010 0101 0001 1110 0111
殖告,系統(tǒng)會將這32位數(shù)當(dāng)作轉(zhuǎn)換后的int類型阿蝶,由于最高位為1
,即這是一個負(fù)數(shù) - 對于系統(tǒng)來說黄绩,如果是負(fù)數(shù)羡洁,則用這個負(fù)數(shù)的補碼表示,即這個負(fù)數(shù)絕對值的二進(jìn)制按位取反爽丹,然后最后一位執(zhí)行不進(jìn)位+1的來的筑煮,所以對于上面這個二進(jìn)制,將其轉(zhuǎn)為10進(jìn)制的過程如下:
- 最高位符號位為1习劫,表示負(fù)數(shù)
- 既然是負(fù)數(shù),最后一位不退位-1嚼隘,得到:
011 1101 1011 0010 0101 0001 1110 0110
- 取補碼:
100 0010 0100 1101 1010 1110 0001 1001
- 表示為十進(jìn)制:
-1112387097
- 至此诽里,就可以解釋為什么
1596615447015.333 | 0 = -1112387097
了。
為了驗證上述過程飞蛹,我們再舉一個例子:1590015447015.123 >> 0 = 877547495
-
1590015447015.123
的二進(jìn)制表示為:10111001000110100010011100100111111100111.000111111
- 舍去其小數(shù)部分后谤狡,從后往前取32位為:
00110100010011100100111111100111
- 最高位為0,正數(shù)卧檐,直接轉(zhuǎn)為10進(jìn)制為:
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
是不是很驚喜!4「 帆调!位運算的效率果然會領(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é)
- 對于獲取系統(tǒng)時間來說诱告,通過
performance
實現(xiàn)性能最高 - 對于取整運算來說撵枢,
位運算
的效率最高 - 盡可能減少
除法
的使用,因為它效率最慢
所以精居,獲取系統(tǒng)10位時間戳最快的方式就是下面這一句:
const performance = require('perf_hooks').performance;
(performance.timeOrigin + performance.now()) / 1000 >> 0