微基准测试进入Java 9
我已經幾個月沒有在這里寫文章了,這種例外也會繼續下去。 我計劃在明年三月左右恢復寫作。 本文末尾的說明。 等待! 不完全是最后,因為您可以向下滾動。 它在文章結尾處。 繼續閱讀!
三年前,我在寫有關Java編譯器如何優化其執行代碼的文章。 或者更確切地說, javac如何做到這一點,而JIT同時做到了。 我制定了一些基準,如Esko Luontola所提到的那樣,確實有些糟糕。 這些基準旨在表明JIT甚至可以在收集有關代碼執行的重要統計數據之前進行優化。
該文章創建于2013年1月。兩個月后, JMH (Java Microbenchmark Harness)的第一個源代碼上傳就發生了。 從那時起,這個工具就發展了很多,并在明年成為Java下一個版本的一部分。 我有一份合同要寫一本有關Java 9的書 ,其中的第5章應該涵蓋Java 9進行微基準測試的可能性。 這是開始與JMH合作的好理由。
在詳細介紹如何使用JMH及其好處之前,讓我們先談談一些微基準測試。
微基準測試
微基準測試正在衡量某些小代碼片段的性能。 它很少使用,在開始為實際的商業環境做微基準測試之前,我們必須三思。 請記住,過早的優化是萬惡之源。 一些開發人員對此聲明進行了概括,稱優化本身是萬惡之源,這也許是事實。 特別是如果我們指的是微基準測試。
微基準測試是一種誘使工具,可以在不知道是否值得優化代碼的情況下優化一些小東西。 當我們有一個包含多個模塊的龐大應用程序時,它可以在多個服務器上運行,我們如何確定改進應用程序的某些特殊部分可以大大提高性能? 它是否會償還增加的收入,以產生如此多的利潤,以彌補我們在性能測試和開發中花費的成本? 我不愿意說你不知道,只是因為這樣的說法太籠統了。 從統計學上幾乎可以肯定,這種包括微基準測試在內的優化在大多數情況下不會使您感到痛苦。 它會很疼,您可能不會注意到它,甚至不會享受它,但這是一個完全不同的故事。
何時使用微基準測試? 我可以看到三個方面:
第一個笑話。 是否可以:您可以嘗試使用微基準測試,以了解其工作原理,然后了解Java代碼如何工作,哪些運行速度快,哪些運行速度不快。 去年, Takipi發表了一篇文章,他們試圖測量Lambda的速度。 閱讀這篇非常好的文章,并清楚地證明了博客相對于為印刷品寫東西的主要優勢。 讀者評論并指出了錯誤,并在本文中進行了更正。
第二是通常的情況。 好的,在讀者發表評論之前,糾正了我的觀點:第二種應該是通常的情況。 第三是在開發庫時,您只是不知道將使用該庫的所有應用程序。 在這種情況下,您將嘗試優化您認為對大多數可想象的應用程序最關鍵的部分。 即使在這種情況下,最好還是使用一些示例應用程序。
陷阱
微基準測試的陷阱是什么? 基準測試是作為實驗完成的。 我編寫的第一個程序是TI計算器代碼,我可以算出該程序為分解兩個大(當時為10位)素數的步數。 即使在那個時候,我仍在使用舊的俄羅斯秒表來測量懶惰的時間來計算步數。 實驗和測量更加容易。
今天,您無法計算CPU執行的步驟數。 程序員無法控制的因素很多,它們可能會改變應用程序的性能,因此無法計算步驟。 我們將測量留給了我們,并且獲得了所有測量的所有問題。
測量的最大問題是什么? 我們對某物感興趣,比如說X,我們通常無法衡量。 因此,我們改為測量Y,并希望Y和X的值耦合在一起。 我們要測量房間的長度,但是要測量激光束從一端到達另一端所花費的時間。 在這種情況下,長度X和時間Y緊密耦合。 很多時候,X和Y僅或多或少地相關。 在大多數情況下,人們進行測量時,X和Y根本不相關。 人們仍然把錢和更多的錢花在這種測量支持的決策上。 以政治選舉為例。
微基準測試沒有什么不同。 很難做到這一點。 如果您對細節和可能的陷阱感興趣, Aleksey Shipilev會提供一個不錯的一小時視頻。 第一個問題是如何衡量執行時間。 小代碼運行時間短,并且在測量開始和結束時System.currentTimeMillis()可能只是返回相同的值,因為我們仍處于同一毫秒內。 即使執行時間為10ms,純粹由于我們測量時間的量化,測量誤差仍然至少為10%。 幸運的是有System.nanoTime() 。 我們開心嗎,文森特?
并不是的。 如文檔所述, nanoTime() 返回正在運行的Java虛擬機的高分辨率時間源的當前值,以納秒為單位。 什么是“當前”? 何時進行調用? 還是退回時? 還是介于兩者之間? 選擇所需的一個,您可能仍然會失敗。 所有Java實現都應保證在最近1000ns內該當前值相同。
文檔中使用nanoTime()之前的另一個警告: 跨越大約292年(263納秒)的連續調用中的差異由于數值溢出而無法正確計算經過時間。
292年? 真?
還有其他問題。 啟動Java代碼時,代碼的前幾千次執行將在沒有運行時優化的情況下進行解釋或執行。 與靜態編譯語言(如Swift,C,C ++或Golang)的編譯器相比,JIT的優勢在于,它可以從代碼執行中收集運行時信息,并且當發現上次執行的編譯基于最近的版本可能會更好運行時統計信息將再次編譯代碼。 對于也嘗試使用統計信息調整其操作參數的垃圾收集可能同樣如此。 因此,編寫良好的服務器應用程序會隨著時間的推移獲得一些性能。 它們的啟動速度稍慢,然后變得更快。 如果重新啟動服務器,則整個迭代將再次開始。
如果您執行微型基準測試,則應注意這種行為。 您是要測量應用程序在預熱期間的性能還是在操作過程中如何真正執行?
解決方案是嘗試考慮所有這些警告的微型基準測試工具。 Java 9是JMH。
什么是JMH?
“ JMH是用于構建,運行和分析以Java和其他針對JVM的其他語言編寫的nano / micro / milli / macro基準測試的Java工具?!?(摘自JMH的官方網站 )
您可以將jmh作為獨立于您測量的實際項目的獨立項目運行,也可以僅將測量代碼存儲在單獨的目錄中。 該線束將根據生產類文件進行編譯并執行基準測試。 如我所見,最簡單的方法是使用Gradle插件執行JMH。 您將基準測試代碼存儲在名為jmh的目錄中(與main和test處于同一級別),并創建可以啟動基準測試的main 。
import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; import java.io.IOException;public class MicroBenchmark {public static void main(String... args) throws IOException, RunnerException {Options opt = new OptionsBuilder().include(MicroBenchmark.class.getSimpleName()).forks(1).build();new Runner(opt).run();}有一個不錯的構建器界面用于配置,還有一個可以執行基準測試的Runner類。
玩一點
在《 Java 9編程實例》一書中,其中一個例子是Mastermind游戲 。 第五章是關于并行解決游戲以加快猜測速度的所有內容。 (如果您不了解該游戲,請在Wikipedia上閱讀它,我不想在這里解釋它,但是您需要它來理解以下內容。)
正常的猜測很簡單。 有一個隱藏的秘密。 秘訣是從6種顏色中選擇4種不同顏色的釘子。 當我們猜測時,我們一個接一個地考慮可能的顏色變化,并向表格提出問題:如果這種選擇是秘密,所有答案都是正確的嗎? 換句話說:這個猜測可以隱藏起來嗎,或者以前的答案在答案中有矛盾嗎? 如果可以將這種猜測作為秘密,那么我們將嘗試將釘子放在桌子上。 答案可能是4/0(alleluia)或其他。 在后一種情況下,我們繼續搜索。 這樣,可以通過五個步驟解決6色4列表格。
為了簡化和可視化,我們用數字命名顏色,例如01234456789 (在jmh基準中有10種顏色,因為6種顏色還不夠)和6種釘子。 這個秘密,我們使用是987654 ,因為這是最后的猜測,我們從去123456 , 123457等。
1983年8月,當我在瑞典學校計算機(ABC80)上使用BASIC語言首次編寫此游戲時,在運行于40MHz 6種顏色,4個位置的z80處理器上,每次猜測都花了20到30秒。 如今,我的MacBook Pro可以使用10種顏色和6種釘子,單線程大約每秒7次玩整個游戲。 但是,當我的機器中有4個處理器支持8個并行線程時,這還不夠。
為了加快執行速度,我將猜測空間劃分為相等的間隔,并啟動了單獨的猜測器,每個猜測器將猜測分散到阻塞隊列中。 主線程從隊列中讀取并在猜測出現時將其放在表上。 萬一某些線程創建一個猜測而主線程嘗試將其用作猜測時已過時,則可能需要一些后期處理,但我們仍然希望可以大大提高速度。
真的加快了猜測速度嗎? 那是JMH的目的。
為了運行基準測試,我們需要一些可以實際執行游戲的代碼
@State(Scope.Benchmark)public static class ThreadsAndQueueSizes {@Param(value = {"1", "4", "8", "16", "32"})String nrThreads;@Param(value = { "1", "10", "100", "1000000"})String queueSize;}@Benchmark@Fork(1)public void playParallel(ThreadsAndQueueSizes t3qs) throws InterruptedException {int nrThreads = Integer.valueOf(t3qs.nrThreads);int queueSize = Integer.valueOf(t3qs.queueSize);new ParallelGamePlayer(nrThreads, queueSize).play();}@Benchmark@Fork(1)public void playSimple(){new SimpleGamePlayer().play();}JMH框架將多次執行代碼,以測量使用多個參數運行的時間。 將執行方法playParallel來針對playParallel和32個線程運行算法,每個線程的最大隊列長度分別為playParallel和一百萬。 當隊列已滿時,各個猜測者將停止猜測,直到主線程從隊列中拉出至少一個猜測為止。
我懷疑如果我們有很多線程,并且我們不限制隊列的長度,那么工作線程將使用僅基于空表的初始猜測來填充隊列,因此不會帶來太多價值。 執行將近15分鐘后,我們會看到什么?
Benchmark (nrThreads) (queueSize) Mode Cnt Score Error Units MicroBenchmark.playParallel 1 1 thrpt 20 6.871 ± 0.720 ops/s MicroBenchmark.playParallel 1 10 thrpt 20 7.481 ± 0.463 ops/s MicroBenchmark.playParallel 1 100 thrpt 20 7.491 ± 0.577 ops/s MicroBenchmark.playParallel 1 1000000 thrpt 20 7.667 ± 0.110 ops/s MicroBenchmark.playParallel 4 1 thrpt 20 13.786 ± 0.260 ops/s MicroBenchmark.playParallel 4 10 thrpt 20 13.407 ± 0.517 ops/s MicroBenchmark.playParallel 4 100 thrpt 20 13.251 ± 0.296 ops/s MicroBenchmark.playParallel 4 1000000 thrpt 20 11.829 ± 0.232 ops/s MicroBenchmark.playParallel 8 1 thrpt 20 14.030 ± 0.252 ops/s MicroBenchmark.playParallel 8 10 thrpt 20 13.565 ± 0.345 ops/s MicroBenchmark.playParallel 8 100 thrpt 20 12.944 ± 0.265 ops/s MicroBenchmark.playParallel 8 1000000 thrpt 20 10.870 ± 0.388 ops/s MicroBenchmark.playParallel 16 1 thrpt 20 16.698 ± 0.364 ops/s MicroBenchmark.playParallel 16 10 thrpt 20 16.726 ± 0.288 ops/s MicroBenchmark.playParallel 16 100 thrpt 20 16.662 ± 0.202 ops/s MicroBenchmark.playParallel 16 1000000 thrpt 20 10.139 ± 0.783 ops/s MicroBenchmark.playParallel 32 1 thrpt 20 16.109 ± 0.472 ops/s MicroBenchmark.playParallel 32 10 thrpt 20 16.598 ± 0.415 ops/s MicroBenchmark.playParallel 32 100 thrpt 20 15.883 ± 0.454 ops/s MicroBenchmark.playParallel 32 1000000 thrpt 20 6.103 ± 0.867 ops/s MicroBenchmark.playSimple N/A N/A thrpt 20 6.354 ± 0.200 ops/s(分數越高,越好。)它表明,如果啟動16個線程并且在某種程度上限制了隊列的長度,我們將獲得最佳性能。 在一個線程(一個主線程和一個工作線程)上運行并行算法要比單線程實現慢一些。 這似乎沒問題:我們有啟動新線程以及線程之間通信的開銷。 我們擁有的最大性能約為16個線程。 因為我們可以在這臺機器上擁有8個內核,所以我們希望能看到8個內核。為什么?
如果我們用隨機的東西替換標準的密碼987654 (即使對于CPU來說也很無聊)會怎樣?
Benchmark (nrThreads) (queueSize) Mode Cnt Score Error Units MicroBenchmark.playParallel 1 1 thrpt 20 12.141 ± 1.385 ops/s MicroBenchmark.playParallel 1 10 thrpt 20 12.522 ± 1.496 ops/s MicroBenchmark.playParallel 1 100 thrpt 20 12.516 ± 1.712 ops/s MicroBenchmark.playParallel 1 1000000 thrpt 20 11.930 ± 1.188 ops/s MicroBenchmark.playParallel 4 1 thrpt 20 19.412 ± 0.877 ops/s MicroBenchmark.playParallel 4 10 thrpt 20 17.989 ± 1.248 ops/s MicroBenchmark.playParallel 4 100 thrpt 20 16.826 ± 1.703 ops/s MicroBenchmark.playParallel 4 1000000 thrpt 20 15.814 ± 0.697 ops/s MicroBenchmark.playParallel 8 1 thrpt 20 19.733 ± 0.687 ops/s MicroBenchmark.playParallel 8 10 thrpt 20 19.356 ± 1.004 ops/s MicroBenchmark.playParallel 8 100 thrpt 20 19.571 ± 0.542 ops/s MicroBenchmark.playParallel 8 1000000 thrpt 20 12.640 ± 0.694 ops/s MicroBenchmark.playParallel 16 1 thrpt 20 16.527 ± 0.372 ops/s MicroBenchmark.playParallel 16 10 thrpt 20 19.021 ± 0.475 ops/s MicroBenchmark.playParallel 16 100 thrpt 20 18.465 ± 0.504 ops/s MicroBenchmark.playParallel 16 1000000 thrpt 20 10.220 ± 1.043 ops/s MicroBenchmark.playParallel 32 1 thrpt 20 17.816 ± 0.468 ops/s MicroBenchmark.playParallel 32 10 thrpt 20 17.555 ± 0.465 ops/s MicroBenchmark.playParallel 32 100 thrpt 20 17.236 ± 0.605 ops/s MicroBenchmark.playParallel 32 1000000 thrpt 20 6.861 ± 1.017 ops/s由于我們不需要仔細研究所有可能的變化,因此性能得以提高。 如果是一個線程,則增加一倍。 在有多個線程的情況下,增益不是很多。 并請注意,這不會提高代碼本身的速度,只能使用統計的隨機機密來更實際地進行測量。 我們還可以看到,在8個線程中獲得16個線程不再有意義。 僅當我們選擇接近變體結尾的秘密時,這才有意義。 為什么? 根據您在此處看到的內容以及GitHub中提供的源代碼,您可以給出答案。
摘要
計劃于2017年2月發行《 Java 9示例編程 》一書。但是,由于我們生活在一個開放源碼的世界中,因此您可以使發布者控制對1.xx-SNAPSHOT版本的訪問。 現在,我告訴了您在編寫本書代碼時使用的初步GitHub URL,您還可以預購eBook,并提供反饋以幫助我創建更好的書。
翻譯自: https://www.javacodegeeks.com/2016/09/microbenchmarking-comes-java-9.html
總結
以上是生活随笔為你收集整理的微基准测试进入Java 9的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: javaone_JavaOne 2012
- 下一篇: smp架构与numa架构_NUMA架构和