日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

测量时间:从Java到内核再到

發(fā)布時間:2023/12/3 java 37 豆豆
生活随笔 收集整理的這篇文章主要介紹了 测量时间:从Java到内核再到 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

問題陳述

當您深入研究時,即使是最基本的問題也會變得很有趣。 今天,我想深入研究一下Java時間。 我們將從Java API的最基礎知識開始,然后逐步降低堆棧:通過OpenJDK源代碼glibc一直到Linux內核。 我們將研究各種環(huán)境下的性能開銷,并嘗試對結果進行推理。

我們將探索經過時間的度量:從某個活動的開始事件到結束事件所經過的時間。 這對于性能改進,操作監(jiān)控和超時執(zhí)行很有用。

以下偽代碼是我們幾乎可以在任何代碼庫中看到的常見用法:

START_TIME = getCurrentTime() executeAction() ELAPSED_TIME = getCurrentTime() - START_TIME

有時它不太明確。 我們可以使用面向方面的編程原則來避免本質上與操作有關的污染我們的業(yè)務代碼,但是它仍然以一種或另一種形式存在。

Java中經過的時間

Java提供了兩個用于測量時間的基本原語: System.currentTimeMillis()和System.nanoTime() 。 這兩個調用之間有幾個區(qū)別,讓我們對其進行分解。

1.起點的穩(wěn)定性

System.currentTimeMillis()返回自Unix紀元開始(1970年1月1日UTC)以來的毫秒數(shù)。 另一方面, System.nanoTime()返回自過去某個任意點以來的納秒數(shù)。

這立即告訴我們currentTimeMillis()的最佳粒度為1毫秒。 它使得不可能測量任何短于1ms的東西。 currentTimeMillis()使用1970年1月1日UTC作為參考點的事實是好事。

為什么好呢? 我們可以比較兩個不同的JVM甚至兩個不同的計算機返回的currentTimeMillis()值。
為什么不好? 當我們的計算機沒有同步時間時,比較將不會很有用。 典型服務器場中的時鐘未完全同步,并且始終會有一些差距。 如果我要比較兩個不同系統(tǒng)的日志文件,這仍然可以接受:如果時間戳記不能完全同步,則可以。 但是,有時這種差距可能導致災難性的結果,例如,當將其用于分布式系統(tǒng)中的沖突解決時。

2.時鐘單調性

另一個問題是,不能保證返回值會單調增加。 這是什么意思? 當您連續(xù)兩次調用currentTimeMillis() ,第二個調用返回的值可能小于第一個。 這是違反直覺的,并且可能導致無意義的結果,例如經過時間為負數(shù)。 顯然, currentTimeMillis()不是衡量應用程序內部經過時間的好選擇。 那nanoTime()呢?

System.nanoTime()不使用Unix紀元作為參考點,而是過去的一些未指定點。 在執(zhí)行單個JVM的過程中,問題仍然存在,僅此而已。 因此,甚至比較在同一臺計算機上運行的兩個不同JVM返回的nanoTime()值也沒有意義,更不用說在單獨的計算機上了。 參考點通常與上一次計算機啟動有關,但這純粹是實現(xiàn)細節(jié),我們根本不能依賴它。 這樣做的好處是,即使計算機中的掛鐘時間由于某種原因而倒退,也不會對nanoTime()產生任何影響。 這就是為什么nanoTime()是一個不錯的工具,可以測量單個JVM上兩個事件之間的經過時間,但是我們無法比較兩個不同JVM上的時間戳。

Java實現(xiàn)

讓我們探討一下Java中如何實現(xiàn)currentTimeMillis()和nanoTime() 。 我將使用來自OpenJDK 14當前負責人的資源 。 System.currentTimeMillis()是一種本地方法,因此我們的Java IDE不會告訴我們它是如何實現(xiàn)的。 這個本地代碼看起來更好一些:

JVM_LEAF(jlong, JVM_CurrentTimeMillis(JNIEnv *env, jclass ignored)) JVMWrapper( "JVM_CurrentTimeMillis" ); return os::javaTimeMillis(); JVM_END

我們可以看到,這只是委派,因為實現(xiàn)因操作系統(tǒng)而異。 這是Linux的實現(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ù)返回一個簡單的結構:

struct timeval { time_t tv_sec; /* seconds */ suseconds_t tv_usec; /* microseconds */ };

該結構包含自該紀元以來的秒數(shù)和給定秒數(shù)內的微秒數(shù)。 currentTimeMillis()的約定將返回自該紀元以來的毫秒數(shù),因此它必須進行簡單的轉換: jlong(time.tv_sec) * 1000 + jlong(time.tv_usec / 1000)

函數(shù)gettimeofday()由glibc實現(xiàn),它最終會調用Linux內核。 稍后我們將更深入地了解。

讓我們看看nanoTime()的實現(xiàn)方式:事實并沒有太大不同System.nanoTime()也是一種本地方法: public static native long nanoTime(); 和jvm.cpp委托給特定于操作系統(tǒng)的實現(xiàn):

JVM_LEAF(jlong, JVM_NanoTime(JNIEnv *env, jclass ignored)) JVMWrapper( "JVM_NanoTime" ); return os::javaTimeNanos(); JVM_END

javaTimeNanos()的Linux實現(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; } }

有兩個分支:如果操作系統(tǒng)支持單調時鐘,它將使用它,否則它將委托給我們的老朋友gettimeofday() 。 Gettimeofday()與Posix調用的System.currentTimeMillis()相同! 顯然,隨著nanoTime()粒度更高,轉換看起來有些不同,但這是相同的Posix調用! 這意味著在某些情況下, System.nanoTime()使用Unix紀元作為參考,因此它可以回到過去! 換句話說:它不能保證是單調的!

好消息是,據(jù)我所知,所有現(xiàn)代Linux發(fā)行版都支持單調時鐘。 我認為該分支是為了與早期版本的kernel / glibc兼容。 如果您對HotSpot如何檢測操作系統(tǒng)是否支持單調時鐘的詳細信息感興趣,請參見此代碼 。 對我們大多數(shù)人來說,重要的是要知道OpenJDK實際上總是調用Posix函數(shù)clock_gettime() ,該函數(shù)在glibc和Linux內核的glibc委托中實現(xiàn)。

基準I –本地筆記本電腦

至此,我們對如何實現(xiàn)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()測量花費少于幾十納秒的任何東西可能是不明智的,因為我們儀器的開銷會高于所測量的間隔。 我們還應該避免在緊密的循環(huán)中使用nanoTime() ,因為延遲會Swift增加。 另一方面,使用nanoTime()來衡量例如來自遠程服務器的響應時間或昂貴的計算時間似乎是明智的。

基準II – AWS

在便攜式計算機上運行基準測試很方便,但不是很實用,除非您愿意放棄便攜式計算機并將其用作應用程序的生產環(huán)境。 相反,讓我們在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

這幾乎與筆記本電腦上的一樣,還不錯。 現(xiàn)在讓我們嘗試c3.large實例。 它是較老的一代,但仍經常使用:

基準測試 模式 碳納米管 得分了 錯誤 單位
板凳 平均 25 362.491 ±0.072 ns / op
Benchnano 平均 25 367.348 ±6.100 ns / op

這看起來一點都不好! c3.large是一個較舊的較小實例,因此預計會有所降低,但這太多了! currentTimeMillis()和nanoTime()都慢一個數(shù)量級。 起初360 ns聽起來可能還不錯,但是請考慮一下:要僅測量一次經過時間,您需要兩次調用。 因此,每次測量花費大約0.7μs。 如果您有10個探針測量不同的執(zhí)行階段,則您的時間為7μs。 透視一下:40gbit網卡的往返行程約為10μs。 這意味著向我們的熱路徑添加一堆探針可能會對延遲產生非常大的影響!

一點內核調查

為什么C3實例比筆記本電腦或C5實例慢得多? 事實證明,這與Linux時鐘源有關,更重要的是與glibc-kernel接口有關。 我們已經知道,每次調用nanoTime()或currentTimeMillis()調用OpenJDK中的本地代碼,該本地代碼調用glibc,后者又調用Linux內核。

有趣的部分是glibc-Linux內核轉換:通常,當進程調用Linux內核函數(shù)(也稱為syscall)時,它涉及從用戶模式切換到內核模式,然后再返回。 此轉換是一個相對昂貴的操作,涉及許多步驟:

  • 將CPU寄存器存儲在內核堆棧中
  • 使用實際功能運行內核代碼
  • 將結果從內核空間復制到用戶空間
  • 從內核堆棧恢復CPU寄存器
  • 跳回用戶代碼

這從來都不是便宜的操作,并且隨著邊信道安全攻擊和相關緩解技術的出現(xiàn),它變得越來越昂貴。

對性能敏感的應用程序通常會盡力避免用戶到內核的轉換。 Linux內核本身提供了一些非常頻繁的系統(tǒng)調用的捷徑,稱為vDSO –虛擬動態(tài)共享對象 。 它實質上導出了一些功能,并將它們映射到進程的地址空間。 用戶進程可以調用這些函數(shù),就像它們是普通共享庫中的常規(guī)函數(shù)??一樣。 結果, clock_gettime()和gettimeofday()都實現(xiàn)了這樣的快捷方式,因此,當glibc調用clock_gettime() ,它實際上只是跳轉到內存地址,而無需執(zhí)行昂貴的用戶到內核轉換。

所有這些聽起來像是一個有趣的理論,但是并不能解釋為什么System.nanoTime()在c3實例上這么慢。

實驗時間

我們將使用另一個出色的Linux工具來監(jiān)視系統(tǒng)調用的數(shù)量: perf 。 我們可以做的最簡單的測試是啟動基準測試并計算操作系統(tǒng)中的所有系統(tǒng)調用。 perf語法很簡單:
sudo perf stat -e raw_syscalls:sys_enter -I 1000 -a
這將為我們提供每秒的系統(tǒng)調用總數(shù)。 一個重要的細節(jié):它將僅向我們提供真正的系統(tǒng)調用,以及完整的用戶模式-內核模式轉換。 vDSO調用不計在內。 這是在c5實例上運行時的外觀:

板凳

您可以看到每秒大約有130個系統(tǒng)調用。 鑒于我們基準測試的每次迭代都少于30 ns,因此很明顯,該應用程序使用vDSO繞過了系統(tǒng)調用。

這是在c3實例上的外觀:

板凳

每秒超過1,300,000個系統(tǒng)調用! 同樣, nanoTime()和currentTimeMillis()的延遲也大約翻了一番,達到700ns /操作。 這是一個相當有力的指示,每個基準測試迭代都會調用一個真實的系統(tǒng)調用!

讓我們使用另一個perf命令來收集其他證據(jù)。 此命令將計算5秒鐘內調用的所有系統(tǒng)調用并按名稱分組:
sudo perf stat -e 'syscalls:sys_enter_*' -a sleep 5
在c5實例上運行時,沒有任何異常情況。 但是,在c3實例上運行時,我們可以看到以下內容:

板凳

這是我們的吸煙槍! 非常有力的證據(jù)表明,當基準測試在c3框上運行時,它將進行真正的gettimeofday()系統(tǒng)調用! 但為什么?

這是 4.4內核(在Ubuntu 16.04中使用) 的相關部分 :

板凳

當Java調用System.currentTimeMillis()時,它是映射到用戶內存并由glibc調用的函數(shù)。 它調用do_realtime() ,該struct tv使用當前時間填充struct tv ,然后返回給調用者。 重要的是所有這些操作都在用戶模式下執(zhí)行,而沒有任何緩慢的系統(tǒng)調用。 好吧,除非do_realtime()返回VCLOCK_NONE 。 在這種情況下,它將調用vdso_fallback_gtod() ,這將執(zhí)行緩慢的系統(tǒng)調用。

為什么c3實例進行回退做系統(tǒng)調用而c5不做? 好吧,這與虛擬化技術的變化有關! 自成立以來,AWS一直在使用Xen虛擬化 。 大約2年前, 他們宣布從Xen過渡到KVM虛擬化 。 C3實例使用Xen虛擬化,較新的c5實例使用KVM。 對我們而言重要的是,每種技術都使用Linux Clock的不同實現(xiàn)。 Linux在/sys/devices/system/clocksource/clocksource0/current_clocksource顯示當前時鐘源。

這是c3:

板凳

這是c5:

板凳

原來,KVM-時鐘實現(xiàn)套vclock_mode到VCLOCK_PVCLOCK這意味著慢回退分支以上不采取。 Xen時鐘源根本沒有設置此模式 ,而是停留在VCLOCK_NONE 。 這將導致跳入vdso_fallback_gtod()函數(shù),該函數(shù)最終將啟動實際的系統(tǒng)調用!

板凳

關于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代表時間戳記計數(shù)器 ,它是一種非常快速的來源,并且對我們而言重要的是適當?shù)膙DSO實施。 讓我們將c3實例中的時鐘源從Xen切換到TSC:

板凳

檢查它是否真的被切換:

板凳

看起來不錯! 現(xiàn)在,我們可以重新運行基準測試:

基準測試 模式 碳納米管 得分了 錯誤 單位
板凳 平均 25 25.558 ±0.070 ns / op
Benchnano 平均 25 24.101 ±0.037 ns / op

數(shù)字看起來不錯! 實際上比具有kvm-clock的c5實例更好。 每秒系統(tǒng)調用數(shù)與c5實例處于同一級別:

板凳

有些人甚至在使用Xen虛擬化時也建議將時鐘源切換為TSC。 我對它可能產生的副作用知之甚少,但是顯然,即使是一些大公司也在生產中做到了這一點。 顯然,這并不證明它是安全的,但這表明它對某些人有效。

最后的話

我們已經看到了底層實現(xiàn)細節(jié)如何對普通Java調用的性能產生重大影響。 這不僅僅是在微基準測試中可見的理論問題, 實際系統(tǒng)也會受到影響 。 您可以直接在Linux內核源代碼樹中閱讀有關vDSO的更多信息。

沒有我在Hazelcast的出色同事,我將無法進行調查。 這是一支世界一流的團隊,我從他們那里學到了很多東西! 我要感謝布倫丹·格雷格(Brendan Gregg)收集的各種技巧 ,我的記憶力一直很差,布倫丹創(chuàng)造了一個出色的備忘單。

最后但并非最不重要的一點:如果您對性能,運行時或分布式系統(tǒng)感興趣,請關注我 !

翻譯自: https://www.javacodegeeks.com/2019/12/measuring-time-from-java-to-kernel-and-back.html

總結

以上是生活随笔為你收集整理的测量时间:从Java到内核再到的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。