System.currentTimeMillis()性能分析

System.currentTimeMillis()在java中是最常用的獲取系統(tǒng)時(shí)間的方法,它返回的是1970年1月1日0點(diǎn)到現(xiàn)在經(jīng)過的毫秒數(shù)。

在系統(tǒng)性能優(yōu)化的過程中,定位問題的過程發(fā)現(xiàn)它似乎有較大性能損耗,所以本文對(duì)System.currentTimeMillis()做性能分析。

一、測試場景

在兩個(gè)配置不同和操作系統(tǒng)不同的linux系統(tǒng)上分別單線程測試調(diào)用頻率為1ms,100ms的情況,查看對(duì)cpu的性能損耗。測試環(huán)境比較干凈,測試代碼為簡單的for循環(huán)調(diào)用。

對(duì)比結(jié)果如下

機(jī)器配置 操作系統(tǒng) 調(diào)用頻率 CPU使用率
8核CPU Debian 2.6.26-29 1ms 2%~5%
1核VCPU Debian 2.6.32-41 1ms 2%
8核CPU Debian 2.6.26-29 100ms 1%
1核VCPU Debian 2.6.32-41 100ms 1%

數(shù)據(jù)說明:

  1. 極端情況下1ms調(diào)用1次,8核CPU消耗點(diǎn)大概在2~5%左右。

  2. 調(diào)用頻率為100ms時(shí),CPU基本在1%左右。

  3. 數(shù)據(jù)說明單單一個(gè)System.currentTimeMillis()高頻率調(diào)用還是有一定CPU消耗的。對(duì)一個(gè)毫秒級(jí)的接口來說這個(gè)性能損耗不算小。

所以在高并發(fā)的接口中還是應(yīng)該盡量避免高頻調(diào)用。

二、原因分析

針對(duì)System.currentTimeMillis()性能不好的原因分析,有一篇很好的文章The slow currentTimeMillis(),它直接從系統(tǒng)級(jí)、源碼、匯編語言各個(gè)層次全方位的分析。

The slow currentTimeMillis()中我們了解到,執(zhí)行速度緩慢currentTimeMillis()是由兩個(gè)因素造成的:

  • JVM使用gettimeofday()而不是clock_gettime()
  • gettimeofday() 如果使用HPET時(shí)間源,則速度非常慢。

但是,HPET現(xiàn)在不是唯一的時(shí)間源。最常見的時(shí)間源且許多系統(tǒng)使用的是TSC。在我們的項(xiàng)目中,服務(wù)器配置了HPET時(shí)間源,原因在于:此時(shí)間源與NTP客戶端完美集成,可以平滑調(diào)整時(shí)間,而TSC不太穩(wěn)定(我不知道細(xì)節(jié);這是本地Linux大師所說的,我別無選擇,只能相信他們)。其他一些開發(fā)人員可能會(huì)遇到同樣的情況。此外,Java開發(fā)人員無法知道程序?qū)⒃诤畏N時(shí)間運(yùn)行。

然而,如果我們使用TSC時(shí)間源,那么了解結(jié)果如何改變?nèi)匀缓苡腥?。TSC代表時(shí)間戳記計(jì)數(shù)器,它僅僅是自啟動(dòng)以來計(jì)算的CPU周期數(shù)(它只有64位寬,因此它將在2.4GHz時(shí)鐘頻率下在243年內(nèi)回繞)。該值可以使用rdtsc指令讀取。傳統(tǒng)上,這個(gè)值有兩個(gè)問題:

  • 來自不同內(nèi)核或物理處理器的值可能相互移位,因?yàn)樘幚砥骺赡茉诓煌臅r(shí)間開始
  • 處理器的時(shí)鐘頻率可能會(huì)在執(zhí)行期間發(fā)生變化。

第一個(gè)似乎確實(shí)是一個(gè)問題。我嘗試rdtsc從多個(gè)內(nèi)核中立即獲取值,并在寫入某個(gè)內(nèi)存位置時(shí)同步。即使在最好的情況下,我也有幾千個(gè)周期的差異。有時(shí)候更多。但是,如果程序員想要手動(dòng)使用TSC,這只是一個(gè)問題; 在這種情況下,必須相應(yīng)地設(shè)置線程關(guān)聯(lián)。操作系統(tǒng)知道它何時(shí)重新調(diào)度從一個(gè)核心到另一個(gè)核心的線程,因此它可以進(jìn)行所有必要的調(diào)整。

第二個(gè)問題似乎已成為過去。英特爾文檔說:

處理器系列以不同的方式增加時(shí)間戳計(jì)數(shù)器:

  • 對(duì)于Pentium M處理器(系列[06H],型號(hào)[09H,0DH]); 對(duì)于奔騰4處理器,英特爾至強(qiáng)處理器(系列[0FH],型號(hào)[00H,01H或02H]); 對(duì)于P6系列處理器:時(shí)間戳計(jì)數(shù)器隨著每個(gè)內(nèi)部處理器時(shí)鐘周期遞增。內(nèi)部處理器時(shí)鐘周期由當(dāng)前內(nèi)核時(shí)鐘與總線時(shí)鐘比決定。英特爾?SpeedStep?技術(shù)轉(zhuǎn)換也可能影響處理器時(shí)鐘。
  • 對(duì)于奔騰4處理器,英特爾至強(qiáng)處理器(系列[0FH],型號(hào)[03H和更高]); 英特爾Core Solo和英特爾酷睿雙核處理器(系列[06H],型號(hào)[0EH]); 英特爾至強(qiáng)處理器5100系列和英特爾酷睿2雙核處理器(系列[06H],型號(hào)[0FH]); 用于英特爾酷睿2和英特爾至強(qiáng)處理器(家族[06H],DisplayModel [17H]); 對(duì)于Intel Atom處理器(系列[06H],DisplayModel [1CH]):時(shí)間戳計(jì)數(shù)器以恒定速率遞增。

它還說:

處理器對(duì)不變TSC的支持由CPUID.80000007H:EDX [8]表示。

三、多線程下使用System.currentTimeMillis()

currentTimeMillis()基于HPET為640ns(1.5M operations/second)運(yùn)行。這是每個(gè)核心還是整個(gè)系統(tǒng)?讓我們運(yùn)行一個(gè)類似的測試Time.java,但啟動(dòng)N個(gè)線程,其中N在1到24之間(包括雙處理器系統(tǒng)中的核心總數(shù))。

以下是少量線程的結(jié)果:

線程數(shù) 平均時(shí)間/訪問次數(shù),ns 總訪問次數(shù)/秒,mil
1 644 1.55
2 918 2.18
3 1366 2.20
4 1871 2.14

以下是執(zhí)行currentTimeMillis()所有線程計(jì)數(shù)所需的平均時(shí)間:

這看起來非常線性,這讓我們懷疑HPET芯片串行化請(qǐng)求,一次只能服務(wù)一個(gè)。或者,從線程計(jì)數(shù)1到線程計(jì)數(shù)2的轉(zhuǎn)換,性能沒有減半,而是下降了1.5倍,可能略高于1。

以下是系統(tǒng)總體性能的圖表(可在一秒鐘內(nèi)在所有內(nèi)核和處理器上執(zhí)行的調(diào)用次數(shù)):

image

可以看出,從1.5M上升到大約2.1M op/sec,并在那里停留。最初的增長可能與我們?cè)陔p處理器系統(tǒng)上進(jìn)行測試有關(guān)。以下是執(zhí)行限于單處理器(taskset 0x555)時(shí)測得的時(shí)間:

線程數(shù) 平均時(shí)間/訪問次數(shù),ns,雙處理器 平均時(shí)間/訪問次數(shù),ns,單處理器
1 644 596
2 918 1105
3 1366 1672
4 1871 2245

單處理器時(shí)間不顯示一個(gè)和兩個(gè)線程之間的異常步驟; 它大致與線程數(shù)成比例,并且(除了一個(gè)線程的值)比雙處理器時(shí)間長。

多進(jìn)程測試給出了與多線程相似的結(jié)果。

簡而言之,HPET的性能確實(shí)在系統(tǒng)范圍內(nèi)有限。無論我們?nèi)绾畏峙浜诵暮土鞒讨g的負(fù)載,每秒鐘不超過兩百萬次的查詢時(shí)間可以在機(jī)器上執(zhí)行。如果24個(gè)內(nèi)核均勻加載,每個(gè)內(nèi)核每秒可以執(zhí)行低于100K的操作。這意味著使用時(shí)必須還真要小心,currentTimeMillis()Java程序。

一個(gè)側(cè)面說明。由于處理器在使用HPET時(shí)會(huì)相互影響,因此存在潛在的安全問題。一個(gè)進(jìn)程可能會(huì)執(zhí)行緊密循環(huán)調(diào)用gettimeofday,從而導(dǎo)致所有其他進(jìn)程訪問此資源并降低其性能?;蛘?,某些進(jìn)程可能調(diào)用此函數(shù)并使用TSC執(zhí)行其執(zhí)行時(shí)間,當(dāng)其他進(jìn)程查詢當(dāng)前時(shí)間時(shí)檢測此方式,這可能有助于確定其他進(jìn)程執(zhí)行的執(zhí)行路徑。

基于TSC的計(jì)時(shí)器不存在此行為。它的性能非常穩(wěn)定,只有在使用所有內(nèi)核(包括超線程內(nèi)核)時(shí)才會(huì)降低40%

四、解決方案

那有什么策略既可以獲取系統(tǒng)時(shí)間又避免高頻率調(diào)用呢。

  • 策略一:如果對(duì)時(shí)間精確度要求不高的話可以使用獨(dú)立線程緩存時(shí)間戳。
  • 策略二:使用Linux的clock_gettime()方法。
Java VM可以使用這個(gè)調(diào)用并且提供更快的速度currentTimeMillis()。
如果絕對(duì)必要,可以使用JNI自己實(shí)現(xiàn)它.
  • 策略三:使用System.nanoTime()。

策略一實(shí)現(xiàn)代碼:

class MillisecondClock {  
    private long rate = 0;// 頻率  
    private volatile long now = 0;// 當(dāng)前時(shí)間  
  
    private MillisecondClock(long rate) {  
        this.rate = rate;  
        this.now = System.currentTimeMillis();  
        start();  
    }  
  
    private void start() {  
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                try {  
                    Thread.sleep(rate);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
                now = System.currentTimeMillis();  
            }  
        }).start();  
    }  
  
    public long now() {  
        return now;  
    }  
  
    public static final MillisecondClock CLOCK = new MillisecondClock(10);  
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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