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