java内核_测量时间:从Java到内核再到
java內(nèi)核
問(wèn)題陳述
當(dāng)您深入研究時(shí),即使是最基本的問(wèn)題也會(huì)變得很有趣。 今天,我想深入研究一下Java時(shí)間。 我們將從Java API的最基礎(chǔ)知識(shí)開(kāi)始,然后逐步降低堆棧:通過(guò)OpenJDK源代碼glibc一直到Linux內(nèi)核。 我們將研究各種環(huán)境下的性能開(kāi)銷(xiāo),并嘗試對(duì)結(jié)果進(jìn)行推理。
我們將探索經(jīng)過(guò)時(shí)間的度量:從某個(gè)活動(dòng)的開(kāi)始事件到結(jié)束事件所經(jīng)過(guò)的時(shí)間。 這對(duì)于性能改進(jìn),操作監(jiān)視和超時(shí)執(zhí)行很有用。
以下偽代碼是我們幾乎可以在任何代碼庫(kù)中看到的常見(jiàn)用法:
START_TIME = getCurrentTime() executeAction() ELAPSED_TIME = getCurrentTime() - START_TIME有時(shí)它不太明確。 我們可以使用面向方面的編程原則來(lái)避免本質(zhì)上與操作有關(guān)的污染我們的業(yè)務(wù)代碼,但是它仍然以一種或另一種形式存在。
Java中經(jīng)過(guò)的時(shí)間
Java提供了兩個(gè)用于測(cè)量時(shí)間的基本原語(yǔ): System.currentTimeMillis()和System.nanoTime() 。 這兩個(gè)調(diào)用之間有幾個(gè)區(qū)別,讓我們對(duì)其進(jìn)行分解。
1.起點(diǎn)的穩(wěn)定性
System.currentTimeMillis()返回自Unix紀(jì)元開(kāi)始(1970年1月1日UTC)以來(lái)的毫秒數(shù)。 另一方面, System.nanoTime()返回自過(guò)去某個(gè)任意點(diǎn)以來(lái)的納秒數(shù)。
這立即告訴我們currentTimeMillis()的最佳粒度為1毫秒。 它使得不可能測(cè)量任何短于1ms的東西。 currentTimeMillis()使用1970年1月1日UTC作為參考點(diǎn)的事實(shí)是好事。
為什么好呢? 我們可以比較兩個(gè)不同的JVM甚至兩個(gè)不同的計(jì)算機(jī)返回的currentTimeMillis()值。
為什么不好? 當(dāng)我們的計(jì)算機(jī)沒(méi)有同步時(shí)間時(shí),比較將不會(huì)很有用。 典型服務(wù)器場(chǎng)中的時(shí)鐘未完全同步,并且始終會(huì)有一些差距。 如果我要比較兩個(gè)不同系統(tǒng)的日志文件,這仍然可以接受:如果時(shí)間戳記未完全同步,則可以。 但是,有時(shí)這種差距可能導(dǎo)致災(zāi)難性的結(jié)果,例如,當(dāng)將其用于分布式系統(tǒng)中的沖突解決時(shí)。
2.時(shí)鐘單調(diào)性
另一個(gè)問(wèn)題是,不能保證返回值會(huì)單調(diào)增加。 這是什么意思? 當(dāng)您連續(xù)兩次調(diào)用currentTimeMillis() ,第二個(gè)調(diào)用返回的值可能小于第一個(gè)。 這是違反直覺(jué)的,并且可能導(dǎo)致無(wú)意義的結(jié)果,例如經(jīng)過(guò)時(shí)間為負(fù)數(shù)。 顯然, currentTimeMillis()不是衡量應(yīng)用程序內(nèi)部經(jīng)過(guò)時(shí)間的好選擇。 那nanoTime()呢?
System.nanoTime()不使用Unix紀(jì)元作為參考點(diǎn),而是過(guò)去的一些未指定點(diǎn)。 在執(zhí)行一次JVM的過(guò)程中,問(wèn)題仍然存在,僅此而已。 因此,甚至比較在同一臺(tái)計(jì)算機(jī)上運(yùn)行的兩個(gè)不同JVM返回的nanoTime()值也毫無(wú)意義,更不用說(shuō)在單獨(dú)的計(jì)算機(jī)上了。 參考點(diǎn)通常與上一次計(jì)算機(jī)啟動(dòng)有關(guān),但這純粹是實(shí)現(xiàn)細(xì)節(jié),我們根本不能依賴(lài)它。 這樣做的好處是,即使計(jì)算機(jī)中的掛鐘時(shí)間由于某種原因而倒退,也不會(huì)對(duì)nanoTime()產(chǎn)生任何影響。 這就是為什么nanoTime()是一個(gè)不錯(cuò)的工具,可以測(cè)量單個(gè)JVM上兩個(gè)事件之間的經(jīng)過(guò)時(shí)間,但是我們無(wú)法比較兩個(gè)不同JVM上的時(shí)間戳。
Java實(shí)現(xiàn)
讓我們探討一下Java中如何實(shí)現(xiàn)currentTimeMillis()和nanoTime() 。 我將使用來(lái)自O(shè)penJDK 14當(dāng)前負(fù)責(zé)人的資源 。 System.currentTimeMillis()是一種本地方法,因此我們的Java IDE不會(huì)告訴我們它是如何實(shí)現(xiàn)的。 這個(gè)本地代碼看起來(lái)更好一些:
JVM_LEAF(jlong, JVM_CurrentTimeMillis(JNIEnv *env, jclass ignored)) JVMWrapper( "JVM_CurrentTimeMillis" ); return os::javaTimeMillis(); JVM_END我們可以看到,這只是委派,因?yàn)閷?shí)現(xiàn)因操作系統(tǒng)而異。 這是Linux的實(shí)現(xiàn) :
jlong os::javaTimeMillis() { timeval time; int status = gettimeofday(&time, NULL); assert (status != - 1 , "linux error" ); return jlong(time.tv_sec) * 1000 + jlong(time.tv_usec / 1000 ); }該代碼委托給Posix函數(shù)gettimeofday() 。 該函數(shù)返回一個(gè)簡(jiǎn)單的結(jié)構(gòu):
struct timeval { time_t tv_sec; /* seconds */ suseconds_t tv_usec; /* microseconds */ };該結(jié)構(gòu)包含自該紀(jì)元以來(lái)的秒數(shù)和給定秒數(shù)內(nèi)的微秒數(shù)。 currentTimeMillis()的約定是返回自該紀(jì)元以來(lái)的毫秒數(shù),因此它必須進(jìn)行簡(jiǎn)單的轉(zhuǎn)換: jlong(time.tv_sec) * 1000 + jlong(time.tv_usec / 1000)
函數(shù)gettimeofday()由glibc實(shí)現(xiàn),它最終會(huì)調(diào)用Linux內(nèi)核。 稍后我們將更深入地了解。
讓我們看一下nanoTime()的實(shí)現(xiàn)方式:事實(shí)并沒(méi)有太大不同– System.nanoTime()也是一種本地方法: public static native long nanoTime(); 和jvm.cpp委托給特定于操作系統(tǒng)的實(shí)現(xiàn):
JVM_LEAF(jlong, JVM_NanoTime(JNIEnv *env, jclass ignored)) JVMWrapper( "JVM_NanoTime" ); return os::javaTimeNanos(); JVM_ENDjavaTimeNanos()的Linux實(shí)現(xiàn)非常有趣:
jlong os::javaTimeNanos() { if (os::supports_monotonic_clock()) { struct timespec tp; int status = os::Posix::clock_gettime(CLOCK_MONOTONIC, &tp); assert (status == 0 , "gettime error" ); jlong result = jlong(tp.tv_sec) * ( 1000 * 1000 * 1000 ) + jlong(tp.tv_nsec); return result; } else { timeval time; int status = gettimeofday(&time, NULL); assert (status != - 1 , "linux error" ); jlong usecs = jlong(time.tv_sec) * ( 1000 * 1000 ) + jlong(time.tv_usec); return 1000 * usecs; } }有兩個(gè)分支:如果操作系統(tǒng)支持單調(diào)時(shí)鐘,它將使用它,否則它將委托給我們的老朋友gettimeofday() 。 Gettimeofday()與Posix調(diào)用的System.currentTimeMillis()相同! 顯然,隨著nanoTime()粒度更高,轉(zhuǎn)換看起來(lái)有些不同,但這是相同的Posix調(diào)用! 這意味著在某些情況下, System.nanoTime()使用Unix紀(jì)元作為參考,因此它可以回到過(guò)去! 換句話說(shuō):它不能保證是單調(diào)的!
好消息是,據(jù)我所知,所有現(xiàn)代Linux發(fā)行版都支持單調(diào)時(shí)鐘。 我認(rèn)為該分支是為了與早期版本的kernel / glibc兼容。 如果您對(duì)HotSpot如何檢測(cè)操作系統(tǒng)是否支持單調(diào)時(shí)鐘的詳細(xì)信息感興趣,請(qǐng)參見(jiàn)以下代碼 。 對(duì)于我們大多數(shù)人來(lái)說(shuō),重要的是要知道OpenJDK實(shí)際上總是調(diào)用Posix函數(shù)clock_gettime() ,該函數(shù)在glibc和Linux內(nèi)核的glibc委托中實(shí)現(xiàn)。
基準(zhǔn)I –本地筆記本電腦
至此,我們對(duì)如何實(shí)現(xiàn)nanoTime()和currentTimeMillis()有了一些直覺(jué)。 讓我們看看他們是快閃還是慢速。 這是一個(gè)簡(jiǎn)單的JMH基準(zhǔn):
@BenchmarkMode (Mode.AverageTime) @OutputTimeUnit (TimeUnit.NANOSECONDS) public class Bench { @Benchmark public long nano() { return System.nanoTime(); } @Benchmark public long millis() { return System.currentTimeMillis(); } }當(dāng)我在裝有Ubuntu 19.10的筆記本電腦上運(yùn)行此基準(zhǔn)測(cè)試時(shí),得到以下結(jié)果:
| 板凳 | 平均 | 25 | 29.625 | ±2.172 | ns / op |
| Benchnano | 平均 | 25 | 25.368 | ±0.643 | ns / op |
每個(gè)調(diào)用System.currentTimeMillis()大約需要29納秒,而System.nanoTime()大約需要25納秒。 不好,不可怕。 這意味著使用System.nano()測(cè)量花費(fèi)少于幾十納秒的任何東西可能是不明智的,因?yàn)槲覀儍x器的開(kāi)銷(xiāo)會(huì)高于所測(cè)量的間隔。 我們還應(yīng)該避免在緊密的循環(huán)中使用nanoTime() ,因?yàn)檠舆t會(huì)Swift增加。 另一方面,使用nanoTime()來(lái)衡量例如來(lái)自遠(yuǎn)程服務(wù)器的響應(yīng)時(shí)間或昂貴的計(jì)算時(shí)間似乎是明智的。
基準(zhǔn)II – AWS
在便攜式計(jì)算機(jī)上運(yùn)行基準(zhǔn)測(cè)試很方便,但不是很實(shí)用,除非您愿意放棄便攜式計(jì)算機(jī)并將其用作應(yīng)用程序的生產(chǎn)環(huán)境。 相反,讓我們?cè)贏WS EC2中運(yùn)行相同的基準(zhǔn)測(cè)試。
讓我們使用Ubuntu 16.04 LTS啟動(dòng)一臺(tái)c5.xlarge機(jī)器,并使用出色的SDKMAN工具安裝由AdoptOpenJDK項(xiàng)目上的杰出人士構(gòu)建的Java 13:
板凳板凳結(jié)果如下:
| 板凳 | 平均 | 25 | 28.467 | ±0.034 | ns / op |
| Benchnano | 平均 | 25 | 27.331 | ±0.003 | ns / op |
這幾乎與筆記本電腦上的一樣,還不錯(cuò)。 現(xiàn)在讓我們嘗試c3.large實(shí)例。 它是較老的一代,但仍經(jīng)常使用:
| 板凳 | 平均 | 25 | 362.491 | ±0.072 | ns / op |
| Benchnano | 平均 | 25 | 367.348 | ±6.100 | ns / op |
這看起來(lái)一點(diǎn)都不好! c3.large是一個(gè)較早且較小的實(shí)例,因此預(yù)計(jì)會(huì)有所降低,但這太多了! currentTimeMillis()和nanoTime()都慢一個(gè)數(shù)量級(jí)。 起初360 ns聽(tīng)起來(lái)可能還不錯(cuò),但是請(qǐng)考慮一下:要僅測(cè)量一次經(jīng)過(guò)時(shí)間,您需要兩次調(diào)用。 因此,每次測(cè)量花費(fèi)大約0.7μs。 如果您有10個(gè)探針測(cè)量不同的執(zhí)行階段,則您的時(shí)間為7μs。 透視一下:40gbit網(wǎng)卡的往返行程約為10μs。 這意味著向我們的熱路徑添加一堆探針可能會(huì)對(duì)延遲產(chǎn)生非常大的影響!
一點(diǎn)內(nèi)核調(diào)查
為什么C3實(shí)例比筆記本電腦或C5實(shí)例慢得多? 事實(shí)證明,這與Linux時(shí)鐘源有關(guān),更重要的是與glibc-kernel接口有關(guān)。 我們已經(jīng)知道,每次調(diào)用nanoTime()或currentTimeMillis()調(diào)用OpenJDK中的本地代碼,該本地代碼調(diào)用glibc,后者又調(diào)用Linux內(nèi)核。
有趣的部分是glibc-Linux內(nèi)核轉(zhuǎn)換:通常,當(dāng)進(jìn)程調(diào)用Linux內(nèi)核函數(shù)(也稱(chēng)為syscall)時(shí),它涉及從用戶(hù)模式切換到內(nèi)核模式,然后再返回。 此過(guò)渡是一個(gè)相對(duì)昂貴的操作,涉及許多步驟:
- 將CPU寄存器存儲(chǔ)在內(nèi)核堆棧中
- 使用實(shí)際功能運(yùn)行內(nèi)核代碼
- 將結(jié)果從內(nèi)核空間復(fù)制到用戶(hù)空間
- 從內(nèi)核堆?;謴?fù)CPU寄存器
- 跳回用戶(hù)代碼
這從來(lái)都不是便宜的操作,并且隨著邊信道安全攻擊和相關(guān)緩解技術(shù)的出現(xiàn),它變得越來(lái)越昂貴。
對(duì)性能敏感的應(yīng)用程序通常會(huì)盡力避免用戶(hù)到內(nèi)核的轉(zhuǎn)換。 Linux內(nèi)核本身提供了一些非常頻繁的系統(tǒng)調(diào)用的捷徑,稱(chēng)為vDSO –虛擬動(dòng)態(tài)共享對(duì)象 。 它實(shí)質(zhì)上導(dǎo)出了一些功能,并將它們映射到進(jìn)程的地址空間中。 用戶(hù)進(jìn)程可以調(diào)用這些函數(shù),就像它們是普通共享庫(kù)中的常規(guī)函數(shù)??一樣。 事實(shí)證明, clock_gettime()和gettimeofday()都實(shí)現(xiàn)了這樣的快捷方式,因此,當(dāng)glibc調(diào)用clock_gettime() ,它實(shí)際上只是跳轉(zhuǎn)到內(nèi)存地址而無(wú)需進(jìn)行昂貴的用戶(hù)到內(nèi)核轉(zhuǎn)換。
所有這些聽(tīng)起來(lái)像是一個(gè)有趣的理論,但是并不能解釋為什么System.nanoTime()在c3實(shí)例上這么慢。
實(shí)驗(yàn)時(shí)間
我們將使用另一個(gè)出色的Linux工具來(lái)監(jiān)視系統(tǒng)調(diào)用的數(shù)量: perf 。 我們可以做的最簡(jiǎn)單的測(cè)試是啟動(dòng)基準(zhǔn)測(cè)試并計(jì)算操作系統(tǒng)中的所有系統(tǒng)調(diào)用。 perf語(yǔ)法很簡(jiǎn)單:
sudo perf stat -e raw_syscalls:sys_enter -I 1000 -a
這將為我們提供每秒的系統(tǒng)調(diào)用總數(shù)。 一個(gè)重要的細(xì)節(jié):它將僅向我們提供真正的系統(tǒng)調(diào)用,以及完整的用戶(hù)模式-內(nèi)核模式轉(zhuǎn)換。 vDSO調(diào)用不計(jì)算在內(nèi)。 這是在c5實(shí)例上運(yùn)行時(shí)的外觀:
您可以看到每秒大約有130個(gè)系統(tǒng)調(diào)用。 鑒于我們基準(zhǔn)測(cè)試的每次迭代都少于30 ns,因此很明顯,該應(yīng)用程序使用vDSO繞過(guò)了系統(tǒng)調(diào)用。
這是在c3實(shí)例上的外觀:
板凳每秒超過(guò)1,300,000個(gè)系統(tǒng)調(diào)用! 同樣, nanoTime()和currentTimeMillis()的延遲大約翻了一番,達(dá)到700ns /操作。 這是每個(gè)基準(zhǔn)測(cè)試迭代都會(huì)調(diào)用真實(shí)系統(tǒng)調(diào)用的有力指示!
讓我們使用另一個(gè)perf命令來(lái)收集其他證據(jù)。 此命令將計(jì)算5秒鐘內(nèi)調(diào)用的所有系統(tǒng)調(diào)用并按名稱(chēng)分組:
sudo perf stat -e 'syscalls:sys_enter_*' -a sleep 5
在c5實(shí)例上運(yùn)行時(shí),沒(méi)有任何異常情況。 但是,在c3實(shí)例上運(yùn)行時(shí),我們可以看到以下內(nèi)容:
這是我們的吸煙槍! 非常有力的證據(jù)表明,當(dāng)基準(zhǔn)測(cè)試在c3框上運(yùn)行時(shí),它將進(jìn)行真正的gettimeofday()系統(tǒng)調(diào)用! 但為什么?
這是 4.4內(nèi)核(在Ubuntu 16.04中使用) 的相關(guān)部分 :
板凳它是映射到用戶(hù)內(nèi)存中的函數(shù),當(dāng)Java調(diào)用System.currentTimeMillis()時(shí)由glibc調(diào)用。 它調(diào)用do_realtime() ,該struct tv使用當(dāng)前時(shí)間填充struct tv ,然后返回給調(diào)用者。 重要的是所有這些操作均在用戶(hù)模式下執(zhí)行,而沒(méi)有任何緩慢的系統(tǒng)調(diào)用。 好吧,除非do_realtime()返回VCLOCK_NONE 。 在這種情況下,它將調(diào)用vdso_fallback_gtod() ,這將執(zhí)行緩慢的系統(tǒng)調(diào)用。
為什么c3實(shí)例執(zhí)行回退做系統(tǒng)調(diào)用而c5不做? 好吧,這與虛擬化技術(shù)的變化有關(guān)! 自成立以來(lái),AWS一直在使用Xen虛擬化 。 大約2年前, 他們宣布從Xen過(guò)渡到KVM虛擬化 。 C3實(shí)例使用Xen虛擬化,較新的c5實(shí)例使用KVM。 對(duì)我們而言重要的是,每種技術(shù)都使用Linux Clock的不同實(shí)現(xiàn)。 Linux在/sys/devices/system/clocksource/clocksource0/current_clocksource顯示當(dāng)前時(shí)鐘源。
這是c3:
板凳這是c5:
板凳原來(lái),KVM-時(shí)鐘實(shí)現(xiàn)套vclock_mode到VCLOCK_PVCLOCK這意味著慢回退分支以上不采取。 Xen時(shí)鐘源根本沒(méi)有設(shè)置此模式 ,而是停留在VCLOCK_NONE 。 這將導(dǎo)致跳入vdso_fallback_gtod()函數(shù),該函數(shù)最終將啟動(dòng)實(shí)際的系統(tǒng)調(diào)用!
板凳 關(guān)于Linux的好處是它是高度可配置的,并且經(jīng)常給我們足夠的繩索來(lái)吊死自己。 我們可以嘗試更改c3上的時(shí)鐘源并重新運(yùn)行基準(zhǔn)測(cè)試。 可通過(guò)$ cat /sys/devices/system/clocksource/clocksource0/available_clocksource
xen tsc hpet acpi_pm $ cat /sys/devices/system/clocksource/clocksource0/available_clocksource
xen tsc hpet acpi_pm
TSC代表時(shí)間戳計(jì)數(shù)器 ,它是一種非常快速的資源,并且對(duì)我們而言重要的是適當(dāng)?shù)膙DSO實(shí)施。 讓我們將c3實(shí)例中的時(shí)鐘源從Xen切換到TSC:
板凳檢查它是否真的被切換:
板凳看起來(lái)挺好的! 現(xiàn)在我們可以重新運(yùn)行基準(zhǔn)測(cè)試:
| 板凳 | 平均 | 25 | 25.558 | ±0.070 | ns / op |
| Benchnano | 平均 | 25 | 24.101 | ±0.037 | ns / op |
數(shù)字看起來(lái)不錯(cuò)! 實(shí)際上比具有kvm-clock的c5實(shí)例更好。 每秒系統(tǒng)調(diào)用數(shù)與c5實(shí)例處于同一級(jí)別:
板凳有人建議即使使用Xen虛擬化,也要將時(shí)鐘源切換為T(mén)SC。 我對(duì)它可能產(chǎn)生的副作用還不太了解,但是顯然,即使是一些大公司也在生產(chǎn)中做到了這一點(diǎn)。 顯然,這并不證明它是安全的,但這表明它對(duì)某些人有效。
最后的話
我們已經(jīng)看到了底層實(shí)現(xiàn)細(xì)節(jié)如何對(duì)普通Java調(diào)用的性能產(chǎn)生重大影響。 這不僅僅是在微基準(zhǔn)測(cè)試中可見(jiàn)的理論問(wèn)題, 實(shí)際系統(tǒng)也會(huì)受到影響 。 您可以直接在Linux內(nèi)核源代碼樹(shù)中閱讀有關(guān)vDSO的更多信息。
沒(méi)有我在Hazelcast的出色同事,我將無(wú)法進(jìn)行調(diào)查。 這是一支世界一流的團(tuán)隊(duì),我從他們那里學(xué)到了很多東西! 我要感謝布倫丹·格雷格(Brendan Gregg)收集的各種技巧 ,我的記憶力一直很差,布倫丹創(chuàng)造了一個(gè)很棒的備忘單。
最后但并非最不重要的一點(diǎn):如果您對(duì)性能,運(yùn)行時(shí)或分布式系統(tǒng)感興趣,請(qǐng)關(guān)注我 !
翻譯自: https://www.javacodegeeks.com/2019/12/measuring-time-from-java-to-kernel-and-back.html
java內(nèi)核
總結(jié)
以上是生活随笔為你收集整理的java内核_测量时间:从Java到内核再到的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: ai字体背景颜色怎么改变(ai字体背景颜
- 下一篇: java美元兑换,(Java实现) 美元