流性能
當(dāng)我閱讀Angelika Langer的Java性能教程時(shí)-Java 8流有多快? 我簡(jiǎn)直不敢相信,對(duì)于一個(gè)特定的操作,它們花費(fèi)的時(shí)間比循環(huán)要長(zhǎng)15倍。 流媒體性能真的會(huì)那么糟糕嗎? 我必須找出答案!
巧合的是,我最近觀看了一個(gè)有關(guān)微基準(zhǔn)測(cè)試Java代碼的精彩討論 ,因此決定將在這里學(xué)到的東西投入工作。 因此,讓我們看一下流是否真的那么慢。
總覽
和往常一樣,我將以沉悶的序幕開始。 這篇文章將解釋為什么您應(yīng)該對(duì)我在這里介紹的內(nèi)容,我如何產(chǎn)生這些數(shù)字以及如何輕松地重復(fù)和調(diào)整基準(zhǔn)非常謹(jǐn)慎。 如果您不關(guān)心這些,請(qǐng)直接跳至Stream Performance 。
但首先,有兩個(gè)快速提示:所有基準(zhǔn)測(cè)試代碼都在GitHub上發(fā)布,并且此Google電子表格包含結(jié)果數(shù)據(jù)。
序幕
免責(zé)聲明
這篇文章包含許多數(shù)字,并且數(shù)字是欺騙性的。 它們似乎都是科學(xué)的,精確的東西,它們誘使我們專注于它們的相互關(guān)系和解釋。 但是,我們應(yīng)該始終同樣關(guān)注它們的發(fā)展!
我將在下面顯示的數(shù)字是在系統(tǒng)上使用非常特定的測(cè)試用例生成的。 過度概括它們很容易! 我還應(yīng)該補(bǔ)充一點(diǎn),對(duì)于非平凡的基準(zhǔn)測(cè)試技術(shù)(即那些不基于循環(huán)和手動(dòng)System.currentTimeMillis() ),我只有兩天的經(jīng)驗(yàn)。
將您在此處獲得的見解納入心理表現(xiàn)模型時(shí)要格外小心。 隱藏在細(xì)節(jié)中的魔鬼是JVM本身,它是一個(gè)騙人的野獸。 我的基準(zhǔn)測(cè)試很可能成為扭曲數(shù)字的優(yōu)化的犧牲品。
系統(tǒng)
- CPU:英特爾(R)核心(TM)i7-4800MQ CPU @ 2.70GHz
- 內(nèi)存 :Samsung DDR3 16GB @ 1.60GHz(測(cè)試完全在RAM中運(yùn)行)
- 操作系統(tǒng) :Ubuntu 15.04。 內(nèi)核版本3.19.0-26-通用
- 的Java :1.8.0_60
- 捷運(yùn) :1.10.5
基準(zhǔn)測(cè)試
捷運(yùn)
基準(zhǔn)測(cè)試是使用JVM性能團(tuán)隊(duì)本身開發(fā)和使用的Java Microbenchmarking Harness(JMH)創(chuàng)建的。 它有完整的文檔記錄,易于設(shè)置和使用,并且通過示例進(jìn)行的解釋非常棒!
如果您喜歡隨意介紹,您可能會(huì)喜歡2013年Devoxx UK的Aleksey Shipilev的演講 。
設(shè)定
為了產(chǎn)生可靠的結(jié)果,基準(zhǔn)測(cè)試要單獨(dú)和重復(fù)運(yùn)行。 每個(gè)基準(zhǔn)測(cè)試方法都有一個(gè)單獨(dú)的運(yùn)行,該運(yùn)行由幾個(gè)分支組成 ,每個(gè)分支在實(shí)際測(cè)量迭代之前運(yùn)行許多預(yù)熱迭代。
我分別使用50,000、500,000、5,000'000、10'000'000和50'000'000元素運(yùn)行基準(zhǔn)測(cè)試。 除了最后一個(gè)以外,所有的分支都有兩個(gè),分別由五個(gè)預(yù)熱和五個(gè)測(cè)量迭代組成,其中每個(gè)迭代持續(xù)三秒鐘。 最后一個(gè)的一部分運(yùn)行在一個(gè)分支中,進(jìn)行了兩次熱身和三個(gè)測(cè)量迭代,每個(gè)迭代持續(xù)30秒。
Langer的文章指出,它們的數(shù)組填充有隨機(jī)整數(shù)。 我將此與更令人愉快的情況進(jìn)行了比較,在這種情況下,數(shù)組中的每個(gè)int等于其在其中的位置。 兩種情況之間的平均偏差為1.2%,最大差異為5.4%。
由于創(chuàng)建數(shù)百萬個(gè)隨機(jī)整數(shù)會(huì)花費(fèi)大量時(shí)間,因此我選擇僅對(duì)有序序列執(zhí)行大多數(shù)基準(zhǔn)測(cè)試,因此除非另有說明,否則與該情況有關(guān)。
碼
基準(zhǔn)代碼本身可在GitHub上獲得 。 要運(yùn)行它,只需轉(zhuǎn)到命令行,構(gòu)建項(xiàng)目,然后執(zhí)行生成的jar:
建立和運(yùn)行基準(zhǔn)
mvn clean install java -jar target/benchmarks.jar一些簡(jiǎn)單的調(diào)整:
- 在執(zhí)行調(diào)用的末尾添加正則表達(dá)式只會(huì)對(duì)完全限定名稱與該表達(dá)式匹配的基準(zhǔn)方法進(jìn)行基準(zhǔn)測(cè)試; 例如僅運(yùn)行ControlStructuresBenchmark : java -jar target/benchmarks.jar Control
- AbstractIterationBenchmark上的注釋控制執(zhí)行每個(gè)基準(zhǔn)測(cè)試的頻率和時(shí)間
- 常數(shù)NUMBER_OF_ELEMENTS定義要迭代的數(shù)組/列表的長(zhǎng)度
- 調(diào)整CREATE_ELEMENTS_RANDOMLY以在有序數(shù)或隨機(jī)數(shù)數(shù)組之間切換
發(fā)布時(shí)間由巴特下, CC-BY-NC-ND 2.0 。
流性能
重復(fù)實(shí)驗(yàn)
讓我們從觸發(fā)我寫這篇文章的情況開始:在500'000個(gè)隨機(jī)元素的數(shù)組中找到最大值。
SimpleOperationsBenchmark.array_max_for
int m = Integer.MIN_VALUE; for (int i = 0; i < intArray.length; i++)if (intArray[i] > m)m = intArray[i];我注意到的第一件事:我的筆記本電腦的性能比JAX文章所使用的機(jī)器好得多。 這是可以預(yù)料的,因?yàn)樗幻枋鰹椤斑^時(shí)的硬件(雙核,沒有動(dòng)態(tài)超頻)”,但是它讓我很高興,因?yàn)槲覟檫@該死的東西花了足夠的錢。 而不是0.36毫秒,而僅需0.130毫秒即可遍歷整個(gè)陣列。 使用流查找最大值的結(jié)果更有趣:
SimpleOperationsBenchmark.array_max_stream
// article uses 'reduce' to which 'max' delegates Arrays.stream(intArray).max();Langer報(bào)告為此花費(fèi)了5.35 ms的運(yùn)行時(shí)間,與循環(huán)的0.36 ms相比,報(bào)告的運(yùn)行速度降低了x15。 我始終測(cè)量大約560毫秒,因此最終導(dǎo)致“僅” x4.5變慢。 仍然很多。
接下來,本文將迭代列表與流式列表進(jìn)行比較。
SimpleOperationsBenchmark.list_max_for
// for better comparability with looping over the array // I do not use a "for each" loop (unlike the Langer's article); // measurements show that this makes things a little faster int m = Integer.MIN_VALUE; for (int i = 0; i < intList.size(); i++)if (intList.get(i) > m)m = intList.get(i);SimpleOperationsBenchmark.list_max_stream
intList.stream().max(Math::max);for循環(huán)的結(jié)果是6.55毫秒,流的結(jié)果是8.33毫秒。 我的測(cè)量值為0.700毫秒和3.272毫秒。 盡管這會(huì)大大改變其相對(duì)性能,但會(huì)創(chuàng)建相同的順序:
| array_max_for | 0.36 | – | 0.123 | – |
| array_max_stream | 5.35 | 14'861% | 0.599 | 487% |
| list_max_for | 6.55 | 22% | 0.700 | 17% |
| list_max_stream | 8.33 | 27% | 3.272 | 467% |
我將遍歷數(shù)組和列表的迭代之間的明顯區(qū)別歸因于拳擊。 或更確切地說是間接導(dǎo)致的。 基本數(shù)組包含我們需要的值,但是列表由Integers數(shù)組支持,即,對(duì)必須首先解析的所需值的引用。
朗格和我的一系列相對(duì)變化之間的可觀差異(+ 14'861%+ 22%+ 27%與+ 487%+ 17%+ 467%)強(qiáng)調(diào)了她的觀點(diǎn),即“流的性能模型并非微不足道的”。
最后,她的文章進(jìn)行了以下觀察:
我們只比較兩個(gè)整數(shù),在JIT編譯后,它們幾乎不止一個(gè)匯編指令。 因此,我們的基準(zhǔn)測(cè)試說明了元素訪問的成本–不一定是典型情況。 如果應(yīng)用于序列中每個(gè)元素的功能是CPU密集型的,則性能指標(biāo)將發(fā)生重大變化。 您會(huì)發(fā)現(xiàn),如果功能受CPU的限制很大,則for循環(huán)流和順序流之間將不再有可測(cè)量的差異。
因此,讓我們鎖定除整數(shù)比較之外的其他功能。
比較操作
我比較了以下操作:
- max:求最大值。
- sum:計(jì)算所有值的總和; 聚合為int而不考慮溢出。
- 算術(shù):為了對(duì)不太簡(jiǎn)單的數(shù)字運(yùn)算建模,我將這些值與少量的移位和乘法相結(jié)合。
- 字符串:為了模擬創(chuàng)建新對(duì)象的復(fù)雜操作,我將元素轉(zhuǎn)換為字符串,然后逐個(gè)字符對(duì)其進(jìn)行異或。
這些是結(jié)果(對(duì)于50萬個(gè)有序元素;以毫秒為單位):
| 0.123 | 0.700 | 0.186 | 0.714 | 4.405 | 4.099 | 49.533 | 49.943 |
| 0.559 | 3.272 | 1.394 | 3.584 | 4.100 | 7.776 | 52.236 | 64.989 |
這突顯了真正的廉價(jià)比較,甚至加法花費(fèi)的時(shí)間也要長(zhǎng)50%。 我們還可以看到更復(fù)雜的操作如何使循環(huán)和流更緊密地聯(lián)系在一起。 差異從幾乎400%下降到25%。 同樣,數(shù)組和列表之間的差異也大大減少了。 顯然,算術(shù)和字符串運(yùn)算受CPU限制,因此解析引用不會(huì)產(chǎn)生負(fù)面影響。
(不要問我為什么對(duì)數(shù)組元素進(jìn)行流式運(yùn)算要比在它們上循環(huán)要快。我已經(jīng)將頭撞墻了一段時(shí)間了。)
因此,讓我們修復(fù)操作并了解迭代機(jī)制。
比較迭代機(jī)制
訪問迭代機(jī)制的性能至少有兩個(gè)重要變量:其開銷以及是否導(dǎo)致裝箱,這將損害內(nèi)存綁定操作的性能。 我決定嘗試通過執(zhí)行CPU綁定操作來繞過拳擊。 如上所述,算術(shù)運(yùn)算可以在我的機(jī)器上實(shí)現(xiàn)。
迭代是通過for和for-each循環(huán)直接實(shí)現(xiàn)的。 對(duì)于流,我做了一些其他實(shí)驗(yàn):
盒裝和非盒裝流
@Benchmark public int array_stream() {// implicitly unboxedreturn Arrays.stream(intArray).reduce(0, this::arithmeticOperation); }@Benchmark public int array_stream_boxed() {// explicitly boxedreturn Arrays.stream(intArray).boxed().reduce(0, this::arithmeticOperation); }@Benchmark public int list_stream_unbox() {// naively unboxedreturn intList.stream().mapToInt(Integer::intValue).reduce(0, this::arithmeticOperation); }@Benchmark public int list_stream() {// implicitly boxedreturn intList.stream().reduce(0, this::arithmeticOperation); }在此,裝箱和拆箱與數(shù)據(jù)的存儲(chǔ)方式(在數(shù)組中拆箱并在列表中裝箱)無關(guān),而是與流如何處理值無關(guān)。
請(qǐng)注意, boxed將IntStream (僅處理原始int的Stream的專用實(shí)現(xiàn))轉(zhuǎn)換為Stream<Integer> ,即對(duì)象上的流。 這將對(duì)性能產(chǎn)生負(fù)面影響,但程度取決于逃避分析的效果。
由于列表是通用的(即沒有專門的IntArrayList ),因此它返回Stream<Integer> 。 最后一個(gè)基準(zhǔn)測(cè)試方法調(diào)用mapToInt ,該方法返回一個(gè)IntStream 。 這是對(duì)流元素進(jìn)行拆箱的幼稚嘗試。
| 4.405 | 4.099 |
| 4.434 | 4.707 |
| 4.100 | 4.518 |
| 7.694 | 7.776 |
好吧,看那個(gè)! 顯然,幼稚的拆箱確實(shí)有效(在這種情況下)。 我有一些模糊的概念,為什么可能會(huì)這樣,但是我無法簡(jiǎn)潔(或正確)表達(dá)。 想法,有人嗎?
(順便說一句,所有關(guān)于裝箱/拆箱和專門實(shí)現(xiàn)的討論使我更加高興的是Valhalla項(xiàng)目進(jìn)展得如此之好 。)
這些測(cè)試的更具體的結(jié)果是,對(duì)于CPU限制的操作,流似乎沒有相當(dāng)大的性能成本。 在擔(dān)心了很大的缺點(diǎn)之后,這很令人高興。
比較元素?cái)?shù)
通常,結(jié)果在序列長(zhǎng)度不同(從50'000到50'000'000)的運(yùn)行中都非常穩(wěn)定。 為此,我檢查了這些運(yùn)行中每1'000'000個(gè)元素的歸一化性能。
但是令我驚訝的是,隨著序列的增加,性能不會(huì)自動(dòng)提高。 我的想法很簡(jiǎn)單,即認(rèn)為這將使JVM有機(jī)會(huì)應(yīng)用更多優(yōu)化。 相反,有一些明顯的情況是性能實(shí)際上下降了:
| array_max_for | + 44.3% |
| array_sum_for | + 13.4% |
| list_max_for | + 12.8% |
有趣的是,這些是最簡(jiǎn)單的迭代機(jī)制和操作。
勝者是比簡(jiǎn)單操作更復(fù)雜的迭代機(jī)制:
| array_sum_stream | – 84.9% |
| list_max_stream | – 13.5% |
| list_sum_stream | – 7.0% |
這意味著我們?cè)谏厦婵吹降?00'000元素的表對(duì)于50'000'000看起來有點(diǎn)不同(歸一化為1'000'000元素;以毫秒為單位):
| 0.246 | 1.400 | 0.372 | 1.428 | 8.810 | 8.199 | 99.066 | 98.650 |
| 1.118 | 6.544 | 2.788 | 7.168 | 8.200 | 15.552 | 104.472 | 129.978 |
| 0.355 | 1.579 | 0.422 | 1.522 | 8.884 | 8.313 | 93.949 | 97.900 |
| 1.203 | 3.954 | 0.421 | 6.710 | 8.408 | 15.723 | 96.550 | 117.690 |
我們可以看到算術(shù)和字符串運(yùn)算幾乎沒有變化。 但是事情發(fā)生了變化,因?yàn)樽詈?jiǎn)單的最大和求和運(yùn)算需要更多的元素將字段更緊密地結(jié)合在一起。
反射
總而言之,我沒有什么大的啟示。 我們已經(jīng)看到,循環(huán)和流之間的明顯差異僅存在于最簡(jiǎn)單的操作中。 但是,令人驚奇的是,當(dāng)我們涉及到數(shù)百萬個(gè)元素時(shí),差距正在縮小。 因此,在使用流時(shí)幾乎不需要擔(dān)心速度會(huì)大大降低。
但是,仍然存在一些未解決的問題。 最值得注意的是:并行流怎么樣? 然后,我很想知道在哪種操作復(fù)雜度下,我可以看到從依賴于迭代(如sum和max )到獨(dú)立于迭代(如算術(shù) )性能的變化。 我也想知道硬件的影響。 當(dāng)然,它會(huì)改變數(shù)字,但是在質(zhì)量上也會(huì)有所不同嗎?
對(duì)我來說,另一點(diǎn)是微基準(zhǔn)測(cè)試并不是那么困難。 還是這樣,我想除非有人指出我的所有錯(cuò)誤…
翻譯自: https://www.javacodegeeks.com/2015/09/stream-performance.html
總結(jié)
- 上一篇: Linux的高级命令,进一步了解Linu
- 下一篇: [渝粤教育] 西南科技大学 市场经济法律