性能测试流程_流性能
性能測試流程
當(dāng)我閱讀Angelika Langer的Java性能教程時-Java 8流有多快? 我簡直不敢相信,對于一個特定的操作,它們花費的時間比循環(huán)要長15倍。 流媒體性能真的會那么糟糕嗎? 我必須找出答案!
巧合的是,我最近觀看了一個關(guān)于微基準(zhǔn)測試Java代碼的精彩討論 ,因此決定將在這里學(xué)到的東西投入工作。 因此,讓我們看一下流是否真的那么慢。
總覽
和往常一樣,我將以沉悶的序幕開始。 這篇文章將解釋為什么您應(yīng)該對我在這里介紹的內(nèi)容,我如何產(chǎn)生這些數(shù)字以及如何輕松地重復(fù)和調(diào)整基準(zhǔn)非常小心。 如果您不關(guān)心這些,請直接跳至Stream Performance 。
但是首先,有兩個快速提示:所有基準(zhǔn)代碼都在GitHub上,并且此Google電子表格包含結(jié)果數(shù)據(jù)。
序幕
免責(zé)聲明
這篇文章包含許多數(shù)字,并且數(shù)字是欺騙性的。 它們似乎都是科學(xué)的,精確的東西,它們誘使我們專注于它們的相互關(guān)系和解釋。 但是,我們應(yīng)該始終同樣關(guān)注它們的發(fā)展!
我將在下面顯示的數(shù)字是在系統(tǒng)上使用非常特定的測試用例生成的。 過度概括它們很容易! 我還應(yīng)該補充一點,對于非平凡的基準(zhǔn)測試技術(shù)(即那些不基于循環(huán)和手動System.currentTimeMillis() ),我只有兩天的經(jīng)驗。
將您在此處獲得的見解納入心理表現(xiàn)模型時要格外小心。 隱藏在細節(jié)中的魔鬼是JVM本身,它是一個騙人的野獸。 我的基準(zhǔn)測試很可能成為扭曲數(shù)字的優(yōu)化的犧牲品。
系統(tǒng)
- CPU:英特爾(R)核心(TM)i7-4800MQ CPU @ 2.70GHz
- 內(nèi)存 :三星DDR3 16GB @ 1.60GHz(測試完全在內(nèi)存中運行)
- 操作系統(tǒng) :Ubuntu 15.04。 內(nèi)核版本3.19.0-26-通用
- 的Java :1.8.0_60
- 捷運 :1.10.5
基準(zhǔn)測試
捷運
基準(zhǔn)測試是使用JVM性能團隊本身開發(fā)和使用的Java微基準(zhǔn)測試線束(JMH)創(chuàng)建的。 它有完整的文檔記錄,易于設(shè)置和使用,并且通過示例進行的解釋非常棒!
如果您想隨意介紹,可能會喜歡2013年Devoxx UK的Aleksey Shipilev的演講 。
建立
為了創(chuàng)建可靠的結(jié)果,分別運行基準(zhǔn)并反復(fù)進行基準(zhǔn)測試。 每個基準(zhǔn)測試方法都有一個單獨的運行,該運行由幾個分支組成 ,每個分支在實際測量迭代之前運行許多預(yù)熱迭代。
我分別使用50,000、500,000、5,000'000、10'000'000和50'000'000元素運行基準(zhǔn)測試。 除了最后一個以外,所有的分支都有兩個分支,都包含五個預(yù)熱和五個測量迭代,每個迭代的時間為三秒鐘。 最后一個的一部分運行在一個分支中,進行了兩次熱身和三個測量迭代,每個迭代持續(xù)30秒。
Langer的文章指出,它們的數(shù)組填充有隨機整數(shù)。 我將此與更令人愉快的情況進行了比較,在這種情況下,數(shù)組中的每個int等于其在其中的位置。 兩種情況之間的平均偏差為1.2%,最大差異為5.4%。
由于創(chuàng)建數(shù)百萬個隨機整數(shù)會花費大量時間,因此我選擇僅對有序序列執(zhí)行大多數(shù)基準(zhǔn)測試,因此除非另有說明,否則該數(shù)字與該情況有關(guān)。
碼
基準(zhǔn)代碼本身可在GitHub上獲得 。 要運行它,只需轉(zhuǎn)到命令行,構(gòu)建項目,然后執(zhí)行生成的jar:
建立和運行基準(zhǔn)
mvn clean install java -jar target/benchmarks.jar一些簡單的調(diào)整:
- 在執(zhí)行調(diào)用的末尾添加正則表達式只會對完全限定名稱與該表達式匹配的基準(zhǔn)方法進行基準(zhǔn)測試; 例如,僅運行ControlStructuresBenchmark : java -jar target/benchmarks.jar Control
- AbstractIterationBenchmark上的注釋控制執(zhí)行每個基準(zhǔn)測試的頻率和時間
- 常數(shù)NUMBER_OF_ELEMENTS定義要迭代的數(shù)組/列表的長度
- 調(diào)整CREATE_ELEMENTS_RANDOMLY以在有序數(shù)或隨機數(shù)數(shù)組之間切換
發(fā)布時間由巴特下, CC-BY-NC-ND 2.0 。
流性能
重復(fù)實驗
讓我們從觸發(fā)我寫這篇文章的情況開始:在500'000個隨機元素的數(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文章所用的機器好得多。 這是可以預(yù)料的,因為它被描述為“過時的硬件(雙核,沒有動態(tài)超頻)”,但是它讓我很高興,因為我為這該死的東西花了足夠的錢。 而不是0.36毫秒,而僅需0.130毫秒即可遍歷整個陣列。 使用流查找最大值的結(jié)果更加有趣:
SimpleOperationsBenchmark.array_max_stream
// article uses 'reduce' to which 'max' delegates Arrays.stream(intArray).max();Langer報告為此花費了5.35 ms的運行時間,與循環(huán)的0.36 ms相比,報告的運行速度降低了x15。 我一直測量大約560毫秒,因此最終結(jié)果變慢了“僅” x4.5。 仍然很多。
接下來,本文將迭代列表與流式列表進行比較。
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毫秒。 我的測量值為0.700毫秒和3.272毫秒。 盡管這會大大改變其相對性能,但會創(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)致的結(jié)果。 基本數(shù)組包含我們需要的值,但列表由Integers數(shù)組支持,即,對我們必須首先解析的所需值的引用。
朗格與我的一系列相對變化之間的可觀差異(+ 14'861%+ 22%+ 27%與+ 487%+ 17%+ 467%)強調(diào)了她的觀點,即“流的性能模型并非微不足道的”。
最后,她的文章進行了以下觀察:
我們只比較兩個整數(shù),在JIT編譯之后,它們幾乎不止一個匯編指令。 因此,我們的基準(zhǔn)測試說明了元素訪問的成本–不一定是典型情況。 如果應(yīng)用于序列中每個元素的功能是CPU密集型的,則性能指標(biāo)將發(fā)生重大變化。 您會發(fā)現(xiàn),如果功能受CPU的限制很大,則for循環(huán)流和順序流之間將不再有可測量的差異。
因此,讓我們鎖定除整數(shù)比較之外的其他功能。
比較操作
我比較了以下操作:
- max:求最大值。
- sum:計算所有值的總和; 聚合為int而不考慮溢出。
- 算術(shù):為了對不太簡單的數(shù)字運算建模,我將這些值與少量的移位和乘法相結(jié)合。
- 字符串:為了模擬創(chuàng)建新對象的復(fù)雜操作,我將元素轉(zhuǎn)換為字符串,然后逐個字符對其進行異或。
這些是結(jié)果(對于50萬個有序元素;以毫秒為單位):
| 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 |
這突顯了真正的廉價比較,甚至加法花費的時間也要長50%。 我們還可以看到更復(fù)雜的操作如何使循環(huán)和流更緊密地聯(lián)系在一起。 差異從幾乎400%下降到25%。 同樣,數(shù)組和列表之間的差異也大大減少了。 顯然,算術(shù)和字符串運算受CPU限制,因此解析引用不會產(chǎn)生負面影響。
(不要問我,為什么對數(shù)組元素進行流式處理的運算要比在它們上循環(huán)要快。我已經(jīng)將頭撞墻了一段時間了。)
因此,讓我們修復(fù)操作并了解迭代機制。
比較迭代機制
訪問迭代機制的性能至少有兩個重要變量:其開銷以及是否導(dǎo)致裝箱,這將損害內(nèi)存綁定操作的性能。 我決定嘗試通過執(zhí)行CPU綁定操作來繞過拳擊。 如上所述,算術(shù)運算可以在我的機器上實現(xiàn)。
迭代是通過for和for-each循環(huán)直接實現(xià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ù)的存儲方式(在數(shù)組中拆箱并在列表中裝箱)無關(guān),而是與流如何處理值無關(guān)。
請注意, boxed將IntStream (僅處理原始int的Stream的專用實現(xiàn))轉(zhuǎn)換為Stream<Integer> ,即對象上的流。 這將對性能產(chǎn)生負面影響,但程度取決于逃逸分析的效果。
由于列表是通用的(即沒有專門的IntArrayList ),因此它返回Stream<Integer> 。 最后一個基準(zhǔn)測試方法調(diào)用mapToInt ,該方法返回一個IntStream 。 這是對流元素進行拆箱的幼稚嘗試。
| 4.405 | 4.099 |
| 4.434 | 4.707 |
| 4.100 | 4.518 |
| 7.694 | 7.776 |
好吧,看那個! 顯然,幼稚的拆箱確實有效(在這種情況下)。 我有一些模糊的概念,為什么會這樣,但是我無法簡潔(或正確)表達。 想法,有人嗎?
(順便說一句,所有關(guān)于裝箱/拆箱和專門實現(xiàn)的討論使我更加高興的是Valhalla項目進展得如此之好 。)
這些測試的更具體的結(jié)果是,對于CPU限制的操作,流似乎沒有相當(dāng)大的性能成本。 在擔(dān)心了很大的缺點之后,這很令人高興。
比較元素數(shù)
通常,結(jié)果在序列長度不同(從500000到500000 000)的運行中都非常穩(wěn)定。 為此,我檢查了這些運行中每1'000'000個元素的歸一化性能。
但是令我驚訝的是,隨著序列的增加,性能不會自動提高。 我的想法很簡單,即認為這將使JVM有機會應(yīng)用更多優(yōu)化。 相反,有一些明顯的情況是性能實際上下降了:
| array_max_for | + 44.3% |
| array_sum_for | + 13.4% |
| list_max_for | + 12.8% |
有趣的是,這些是最簡單的迭代機制和操作。
勝者是比簡單操作更復(fù)雜的迭代機制:
| array_sum_stream | – 84.9% |
| list_max_stream | – 13.5% |
| list_sum_stream | – 7.0% |
這意味著我們在上面看到的500'000個元素的表對于50'000'000個元素而言有些不同(歸一化為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ù)和字符串運算幾乎沒有變化。 但是事情發(fā)生了變化,因為最簡單的最大和求和運算需要更多的元素使字段更緊密地結(jié)合在一起。
反射
總而言之,我沒有什么大的啟示。 我們已經(jīng)看到,循環(huán)和流之間的明顯差異僅存在于最簡單的操作中。 但是,令人驚奇的是,當(dāng)我們涉及到數(shù)百萬個元素時,差距正在縮小。 因此,在使用流時幾乎不必擔(dān)心速度會大大降低。
但是,仍然存在一些未解決的問題。 最值得注意的是:并行流怎么樣? 然后,我很想知道在哪種操作復(fù)雜度下可以看到從依賴于迭代的性能(例如sum和max )到獨立于迭代(例如算術(shù) )的性能的變化。 我也想知道硬件的影響。 當(dāng)然,它會改變數(shù)字,但是在質(zhì)量上也會有所不同嗎?
對我來說,另一點是微基準(zhǔn)測試并不是那么困難。 還是這樣,我想直到有人指出我所有的錯誤...
翻譯自: https://www.javacodegeeks.com/2015/09/stream-performance.html
性能測試流程
總結(jié)
以上是生活随笔為你收集整理的性能测试流程_流性能的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 字节流和字符流哪个不刷新_不喜欢节流吗?
- 下一篇: 半圆的快捷键(半圆的快捷键尺寸)