摘要:這減輕了手動(dòng)重復(fù)執(zhí)行相同基準(zhǔn)測(cè)試的痛苦,并簡(jiǎn)化了獲取結(jié)果的流程。處理項(xiàng)目的代碼并從標(biāo)有注釋的方法處生成基準(zhǔn)測(cè)試程序。用和運(yùn)行該基準(zhǔn)測(cè)試得到以下結(jié)果。同時(shí),和的基線測(cè)試結(jié)果也有略微的不同。
Java 8 已經(jīng)發(fā)布一段時(shí)間了,許多開發(fā)者已經(jīng)開始使用 Java 8。本文也將討論最新發(fā)布在 JDK 中的并發(fā)功能更新。事實(shí)上,JDK 中已經(jīng)有多處java.util.concurrent 改動(dòng),但本文重點(diǎn)將是 Fork-Join 框架的改進(jìn)。我們將討論一點(diǎn) Fork-Join,然后實(shí)現(xiàn)一個(gè)簡(jiǎn)單的基準(zhǔn)測(cè)試以比較 FJ 在 Java 7 和Java 8 中的性能。
你可能對(duì)Fork/Join在意的地方ForkJoin 是一個(gè)通常用于并行計(jì)算遞歸任務(wù)的框架。它最早被引入Java 7 中,從那時(shí)起它就能很好地完成目標(biāo)任務(wù)。原因在于,許多大型任務(wù)本質(zhì)上都可以遞歸表示。
以最有名的 MapReduce 編程為例:對(duì)一篇文章中不同詞的出現(xiàn)次數(shù)進(jìn)行統(tǒng)計(jì)。很顯然,可以將文檔分為很多部分,逐項(xiàng)地記錄字?jǐn)?shù),最后再合并成結(jié)果。誠(chéng)然,F(xiàn)orkJoin其實(shí)是 MapReduce 基本法則的一種實(shí)現(xiàn),區(qū)別在于,所有的 worker 都是同一個(gè)虛擬機(jī)中的線程,而不是一組機(jī)器。
ForkJoin 框架的核心部分是 ForkJoinPool ,它是一個(gè) ExecutorService, 能夠接收異步任務(wù),返回Future對(duì)象,因此可用于跟蹤執(zhí)行中的計(jì)算狀態(tài)。
使 ForkJoinPool 不同于其他 ExecutorServices 的是,在當(dāng)下并不執(zhí)行任務(wù)的工作線程會(huì)檢查其伙伴的工作狀態(tài),并向他們借取任務(wù)。這種技術(shù)稱為 work-stealing 。那么,work-stealing 有什么妙用呢?
work-stealing 是一種分散式的工作量管理方法,無需將工作單元分配給所有可用的工作線程,而是每個(gè)線程自己管理其任務(wù)隊(duì)列。關(guān)鍵在于高效地管理這些隊(duì)列。
關(guān)于讓每個(gè)工作進(jìn)程處理自己的隊(duì)列,有兩個(gè)主要問題:
外部提交的任務(wù)去哪里了?
我們?cè)鯓咏M織 work-stealing 以有效訪問隊(duì)列
本質(zhì)上來說,在執(zhí)行大型任務(wù)時(shí),外部提交任務(wù)和由工作線程創(chuàng)建的任務(wù)之間區(qū)別不大。他們都有類似的執(zhí)行要求并提供結(jié)果。然而,運(yùn)作方式是不同的。最主要的區(qū)別在于由工作進(jìn)程創(chuàng)建的任務(wù)可以被竊取。這意味著即便被放進(jìn)了一個(gè)工作進(jìn)程的任務(wù)隊(duì)列中,他們?nèi)钥赡鼙黄渌ぷ鬟M(jìn)程執(zhí)行。
ForkJoin 框架處理它的方法很簡(jiǎn)單,每個(gè)工作線程都有2個(gè)任務(wù)隊(duì)列,一個(gè)用于外部任務(wù),另一個(gè)用于實(shí)現(xiàn)竊取工作進(jìn)程的運(yùn)作。當(dāng)外部提交任務(wù)時(shí),會(huì)將任務(wù)添加至隨機(jī)的工作隊(duì)列中。當(dāng)一個(gè)任務(wù)被分為更小的任務(wù)時(shí),工作線程將他們添加到自己的任務(wù)隊(duì)列中,并希望其他工作線程來幫忙。
竊取任務(wù)的想法基于以下事實(shí):工作線程在它任務(wù)隊(duì)列末尾添加任務(wù)。在正常的執(zhí)行過程中,每個(gè)工作線程試著去從任務(wù)隊(duì)列的隊(duì)首拿任務(wù),當(dāng)其個(gè)人隊(duì)列的任務(wù)為空時(shí),這一操作就會(huì)失敗,轉(zhuǎn)而竊取別的工作線程的任務(wù)隊(duì)列末尾的任務(wù)。這有效避免了多數(shù)任務(wù)隊(duì)列的互鎖問題,提高了性能。
另一個(gè)使 ForkJoin 池工作更快的訣竅是當(dāng)一個(gè)工作線程竊取任務(wù)時(shí),它留下了它在哪里取得任務(wù)的線索,這樣原始的工作線程可以找到它并且?guī)椭摴ぷ骶€程,因此父任務(wù)的的工作進(jìn)展會(huì)更快。
總而言之,這是一套極其復(fù)雜的系統(tǒng),需要大量的背景知識(shí)使其順利運(yùn)行。并且,系統(tǒng)的屬性和性能與具體實(shí)現(xiàn)的方式關(guān)系很大。因此筆者懷疑,若不進(jìn)行重大的重構(gòu),系統(tǒng)會(huì)徹底改變。
Java 7 中 ForkJoin 有什么問題?在 Java 7 中引入 ForkJoin 框架之后,它運(yùn)行良好。然而它并沒有停止進(jìn)步。在 Java 8 的并發(fā)性更新中, ForkJoin 得到改善。從這次的 Java 增強(qiáng)方案中,我們可以了解改善的內(nèi)容。
增加了 ForkJoinPools 的功能并提高其性能,使其應(yīng)用在用戶希望的日益廣泛的應(yīng)用中,且效率更高。新特性包括對(duì)最適于 IO-bound 使用的 completion-based 設(shè)計(jì)的支持等。
另一個(gè)消息來源當(dāng)然是與改進(jìn)作者的對(duì)話,例如,Doug Lea 早前曾提到的更新有:
當(dāng)大量的用戶提交大量任務(wù)時(shí),吞吐量能大幅度提高。其原理是將外部提交者與工作線程相似地對(duì)待——均使用隨機(jī)任務(wù)隊(duì)列和竊取任務(wù)。當(dāng)所有任務(wù)都為異步,且被提交至 pool 而不是 forked 時(shí),能極大地提高吞吐量。
然而找出究竟什么被改變了、哪些場(chǎng)景被影響了并不簡(jiǎn)單。因此,讓我們換一種方式解決。筆者會(huì)創(chuàng)建一個(gè)基準(zhǔn)測(cè)試程序以模仿簡(jiǎn)單的 ForkJoin 計(jì)算,并測(cè)量 ForkJoin 處理任務(wù)與單個(gè)線程依次完成任務(wù)各自所需時(shí)間,希望這種方法能幫我們找出改善的具體內(nèi)容。
Java 8 和 Java 7 性能的比較筆者創(chuàng)建了一個(gè)基準(zhǔn)測(cè)試程序以探索 Java 7 和 Java 8 之間的區(qū)別是否真的明顯。如果你想查看源碼,或者親自嘗試,這里是其 Github repo 。
由于Oracle工程師的努力,OpenJDK現(xiàn)在已經(jīng)包含 Java Microbenchmark Harness (JMH)項(xiàng)目,該項(xiàng)目專用于創(chuàng)建基準(zhǔn)測(cè)試程序,且不容易出現(xiàn)常見的微基準(zhǔn)測(cè)試問題與錯(cuò)誤。
JMH 還附帶了 Maven 原型項(xiàng)目。因此,將一切設(shè)置好其實(shí)很簡(jiǎn)單。
org.openjdk.jmh mh-core 0.4.1
在寫本文時(shí),JMH core 的最新版本是 0.4.1 ,包括了 @Param 注釋,可用一系列的參數(shù)化輸入運(yùn)行基準(zhǔn)測(cè)試程序。這減輕了手動(dòng)重復(fù)執(zhí)行相同基準(zhǔn)測(cè)試的痛苦,并簡(jiǎn)化了獲取結(jié)果的流程。
現(xiàn)在,每個(gè)基準(zhǔn)測(cè)試迭代會(huì)獲得自己的 ForkJoinPool 實(shí)例,這也減少了常用 ForkJoinPool 實(shí)例化在 Java 8 與其之前版本中的區(qū)別。
@BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.MILLISECONDS) @Warmup(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) @Measurement(iterations = 20, time = 3, timeUnit = TimeUnit.SECONDS) @Fork(1) @State(Scope.Benchmark) public class FJPBenchmark { @Param({ "200", "400", "800", "1600"}) public int N; public Listtasks; public ForkJoinPool pool = new ForkJoinPool(); @Setup public void init() { Random r = new Random(); r.setSeed(0x32106234567L); tasks = new ArrayList (N * 3); for (int i = 0; i < N; i++) { tasks.add(new Sin(r.nextDouble())); tasks.add(new Cos(r.nextDouble())); tasks.add(new Tan(r.nextDouble())); } } @GenerateMicroBenchmark public double forkJoinTasks() { for (RecursiveTask task : tasks) { pool.submit(task); } double sum = 0; Collections.reverse(tasks); for (RecursiveTask task : tasks) { sum += task.join(); } return sum; } @GenerateMicroBenchmark public double computeDirectly() { double sum = 0; for (RecursiveTask task : tasks) { sum += ((DummyComputableThing) task).dummyCompute(); } return sum; } }
Sin 、Cos 和 Tan 是 RecursiveTask 的實(shí)例,實(shí)際上 Sin 和 Cos 并不遞歸,但會(huì)分別計(jì)算 Math.sin(input) 和 Math.cos(input) 的值 。Tan 的任務(wù)實(shí)際上會(huì)遞歸為一組 Sin 和 Cos ,并返回兩者的除法結(jié)果。
JMH 處理項(xiàng)目的代碼并從標(biāo)有 @GenerateMicroBenchmark 注釋的方法處生成基準(zhǔn)測(cè)試程序。你在該類上方看到的其他注釋指定了基準(zhǔn)測(cè)試的選項(xiàng):迭代次數(shù),計(jì)入最終結(jié)果的迭代次數(shù),是否 fork 另一個(gè) JVM 進(jìn)程用于基準(zhǔn)測(cè)試以及測(cè)量哪些值。測(cè)量值可以是代碼的吞吐量,或這些方法在一段時(shí)間內(nèi)的執(zhí)行次數(shù)。
@Param 指定運(yùn)行基準(zhǔn)測(cè)試程序時(shí)幾個(gè)輸入的大小。總而言之,JMH非常簡(jiǎn)單,創(chuàng)建基準(zhǔn)測(cè)試程序不需要手動(dòng)處理迭代、定時(shí)或整理結(jié)果。
用 Java 7 和 8 運(yùn)行該基準(zhǔn)測(cè)試得到以下結(jié)果。筆者分別使用的是1.7.0_40 and 1.8.0.版本。
shelajev@shrimp ~/repo/blogposts/fork-join-blocking-perf ? java -version java version "1.7.0_40" Java(TM) SE Runtime Environment (build 1.7.0_40-b43) Java HotSpot(TM) 64-Bit Server VM (build 24.0-b56, mixed mode) Benchmark (N) Mode Samples Mean Mean error Units o.s.FJPB.computeDirectly 200 thrpt 20 27.890 0.306 ops/ms o.s.FJPB.computeDirectly 400 thrpt 20 14.046 0.072 ops/ms o.s.FJPB.computeDirectly 800 thrpt 20 6.982 0.043 ops/ms o.s.FJPB.computeDirectly 1600 thrpt 20 3.481 0.122 ops/ms o.s.FJPB.forkJoinTasks 200 thrpt 20 11.530 0.121 ops/ms o.s.FJPB.forkJoinTasks 400 thrpt 20 5.936 0.126 ops/ms o.s.FJPB.forkJoinTasks 800 thrpt 20 2.931 0.027 ops/ms o.s.FJPB.forkJoinTasks 1600 thrpt 20 1.466 0.012 ops/ms shelajev@shrimp ~/repo/blogposts/fork-join-blocking-perf ? java -version java version "1.8.0" Java(TM) SE Runtime Environment (build 1.8.0-b132) Java HotSpot(TM) 64-Bit Server VM (build 25.0-b70, mixed mode) Benchmark (N) Mode Samples Mean Mean error Units o.s.FJPB.computeDirectly 200 thrpt 20 27.680 2.050 ops/ms o.s.FJPB.computeDirectly 400 thrpt 20 13.690 0.994 ops/ms o.s.FJPB.computeDirectly 800 thrpt 20 6.783 0.548 ops/ms o.s.FJPB.computeDirectly 1600 thrpt 20 3.364 0.304 ops/ms o.s.FJPB.forkJoinTasks 200 thrpt 20 15.868 0.291 ops/ms o.s.FJPB.forkJoinTasks 400 thrpt 20 8.060 0.222 ops/ms o.s.FJPB.forkJoinTasks 800 thrpt 20 4.006 0.024 ops/ms o.s.FJPB.forkJoinTasks 1600 thrpt 20 1.968 0.043 ops/ms
為了便于查看結(jié)果,下面以圖表形式進(jìn)行展示。
我們可以看到 JDK 7 與 8 間的基線結(jié)果(直接用同一線程運(yùn)行程序的吞吐量)差異并不大。然而,若加入管理遞歸任務(wù)的時(shí)間,使用 ForkJoin 來執(zhí)行,則 Java 8 的速度更快。這個(gè)簡(jiǎn)單的基準(zhǔn)測(cè)試表明,在最新版的 Java 中,管理 ForkJoin 任務(wù)的效率有了 35% 左右的性能提高。
基線和 FJ 計(jì)算之間的結(jié)果差異是因?yàn)槲覀兛桃鈩?chuàng)建的遞歸任務(wù)非常單薄。該任務(wù)實(shí)質(zhì)上只是調(diào)用一個(gè)優(yōu)化后的數(shù)學(xué)類。因此,直接進(jìn)行數(shù)學(xué)運(yùn)算會(huì)快得多。一個(gè)更強(qiáng)壯的任務(wù)必將改變這一情況,但是它們會(huì)減輕 ForkJoin 管理的開銷,而這是我們起初就想測(cè)量的目標(biāo)。不過,一般而言,執(zhí)行遞歸任務(wù)比多次執(zhí)行同個(gè)方法調(diào)用要高效得多。
同時(shí),Java 7 和 Java 8 的基線測(cè)試結(jié)果也有略微的不同。這個(gè)差異是可以忽視的,但很可能不是因?yàn)?Java 7 和 8 中數(shù)學(xué)類的實(shí)現(xiàn)差異造成的。而是一個(gè)測(cè)量假象,JMH 努力抵消卻還是無法避免。
免責(zé)聲明:當(dāng)然,這些結(jié)果是模擬所得的,你應(yīng)該持保留態(tài)度。然而,除了討論 Java 性能,筆者也想展示 JMH 創(chuàng)建基準(zhǔn)測(cè)試程序是如何簡(jiǎn)單,且能避免一些常見基準(zhǔn)測(cè)試問題,比如沒有提前預(yù)熱 JVM 。如果基準(zhǔn)測(cè)試本身存在缺陷,熱身也無濟(jì)于事,但是肯定還是有所裨益。因此,如果你看到以上代碼中的邏輯缺陷,請(qǐng)一定告訴筆者。
總結(jié):首先,F(xiàn)orkJoinPool, ForkJoinPool.WorkQueue 和ForkJoinTask 類的源碼并不容易閱讀,它包含許非安全原理,因此你可能沒法在15分鐘完全理解ForkJoin 框架。
然而,這些類的文檔豐富,并且包含許多內(nèi)部注釋。它也可能學(xué)習(xí)挖掘JDK最有趣的地方。
另一個(gè)相關(guān)的發(fā)現(xiàn)是 ForkJoinPool 在 Java8 中的性能更好,至少在一些用例中是這樣的。雖然筆者不能精確地描述這背后的原因,但如果我在代碼中用到 ForkJoin ,我一定會(huì)升級(jí) Java 版本。
原文地址:http://zeroturnaround.com/rebellabs/is-java-8-the-fastest-jvm-ever-performance-benchmarking-of-fork-join/ 本文作者:Oleg Shelajev 系 OneAPM 工程師編譯整理。
OneAPM 為您提供端到端的 Java 應(yīng)用性能解決方案,我們支持所有常見的 Java 框架及應(yīng)用服務(wù)器,助您快速發(fā)現(xiàn)系統(tǒng)瓶頸,定位異常根本原因。分鐘級(jí)部署,即刻體驗(yàn),Java 監(jiān)控從來沒有如此簡(jiǎn)單。想閱讀更多技術(shù)文章,請(qǐng)?jiān)L問 OneAPM 官方技術(shù)博客。
本文轉(zhuǎn)自 OneAPM 官方博客
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/65330.html
摘要:我們修改上面代碼,再來看下返回值類型限制的情況運(yùn)行結(jié)果這段代碼我們額外聲明了返回值的類型為型。對(duì)函數(shù)返回值的聲明做了擴(kuò)充,可以定義其返回值為,無論是否開啟嚴(yán)格模式,只要函數(shù)中有以外的其他語句都會(huì)報(bào)錯(cuò)。 順風(fēng)車運(yùn)營(yíng)研發(fā)團(tuán)隊(duì) 王坤 發(fā)表至21CTO公眾號(hào)(https://mp.weixin.qq.com/s/ph...) showImg(https://segmentfault.c...
摘要:減少垃圾收集壓力因?yàn)樗虚L(zhǎng)生命周期的數(shù)據(jù)都是在的管理內(nèi)存中以二進(jìn)制表示的,所以所有數(shù)據(jù)對(duì)象都是短暫的,甚至是可變的,并且可以重用。當(dāng)然,并不是唯一一個(gè)基于且對(duì)二進(jìn)制數(shù)據(jù)進(jìn)行操作的數(shù)據(jù)處理系統(tǒng)。 showImg(https://segmentfault.com/img/remote/1460000020044119?w=1280&h=853); 前言 如今,許多用于分析大型數(shù)據(jù)集的開源系...
摘要:如同其它虛擬機(jī),虛擬機(jī)為字節(jié)碼提供了一個(gè)運(yùn)行時(shí)環(huán)境。編譯是一個(gè)混合模式的虛擬機(jī),也就是說它既可以解釋字節(jié)碼,又可以將代碼編譯為本地機(jī)器碼以更快的執(zhí)行。解決此問題一般是在進(jìn)程啟動(dòng)后,對(duì)代碼進(jìn)行預(yù)熱以使它們被強(qiáng)制編譯。 Java HotSpot虛擬機(jī)是Oracle收購Sun時(shí)獲得的,JVM和開源的OpenJDK都是以此虛擬機(jī)為基礎(chǔ)發(fā)展的。如同其它虛擬機(jī),HotSpot虛擬機(jī)為字節(jié)碼提供了一...
閱讀 1635·2021-10-27 14:13
閱讀 1876·2021-10-11 10:59
閱讀 3374·2021-09-24 10:26
閱讀 1932·2019-08-30 12:48
閱讀 3044·2019-08-30 12:46
閱讀 2038·2019-08-30 11:16
閱讀 1422·2019-08-30 10:48
閱讀 2746·2019-08-29 16:54