這波操作看麻了!一億行數(shù)據(jù),從71s到1.7s的優(yōu)化之路
你好呀,我是歪歪。
春節(jié)期間關(guān)注到了一個(gè)關(guān)于 Java 方面的比賽,很有意思。由于是開(kāi)源的,我把項(xiàng)目拉下來(lái)試圖學(xué)(白)習(xí)(嫖)別人的做題思路,在這期間一度讓我產(chǎn)生了一個(gè)自我懷疑:
他們寫(xiě)的 Java 和我會(huì)的 Java 是同一個(gè) Java 嗎?
不能讓我一個(gè)人懷疑,所以這篇文章我打算帶你盤一下這個(gè)比賽,并且試圖讓你也產(chǎn)生懷疑。
賽題
在 2024 年 1 月 1 日,一個(gè)叫做 Gunnar Morling 的帥哥,發(fā)了這樣一篇文章:
https://www.morling.dev/blog/one-billion-row-challenge/
文章的標(biāo)題叫做《The One Billion Row Challenge》,一億行挑戰(zhàn),簡(jiǎn)稱就是 1BRC,挑戰(zhàn)的時(shí)間是一月份整個(gè)月。
賽題的內(nèi)容非常簡(jiǎn)單,你只需要看懂這個(gè)文件就行了:
圖片
文件的每一行記錄的是一個(gè)氣象站的溫度值。氣象站和溫度分號(hào)分隔,溫度值只會(huì)保留一位小數(shù)。
參賽者只需要解析這個(gè)文件,然后并計(jì)算出每個(gè)氣象站的最小、最大和平均溫度。按照字典序的格式輸出就行了:
圖片
出題人還配了一個(gè)簡(jiǎn)圖:
圖片
需求非常明確、簡(jiǎn)單,對(duì)不對(duì)?
為了讓你徹底明白,我再給你舉一個(gè)具體的例子。
假設(shè)文件中的內(nèi)容是這樣的:
chengdu;12.0
guangzhou;7.2;
chengdu;6.3
beijing;-3.6;
chengdu;23.0
shanghai;9.8;
chengdu;24.3
beijing;17.8;
那么 chengdu (成都)的最低氣溫是 6.3,最高氣溫是 24.3,平均氣溫是(12.0+6.3+23.0+24.3)/4=16.4,就是這么樸實(shí)無(wú)華的計(jì)算方式。
最終結(jié)果輸出的時(shí)候,再注意一下字典序就行。
這有啥好挑戰(zhàn)的呢?
難點(diǎn)在于出題人給出的這個(gè)文件有 10 億行數(shù)據(jù)。
在我的垃圾電腦上,光是跑出題人提供的數(shù)據(jù)生成的腳本,就跑了 20 分鐘:
圖片
跑出來(lái)之后文件大小都有接近 13G,記事本打都打不開(kāi):
圖片
所以挑戰(zhàn)點(diǎn)就在于“一億行”數(shù)據(jù)。
具體的一些規(guī)則描述和細(xì)節(jié)補(bǔ)充,都在 github 上放好了:
https://github.com/gunnarmorling/1brc
針對(duì)這個(gè)挑戰(zhàn),出題人還提供了一個(gè)基線版本:
首先封裝了一個(gè) MeasurementAggregator 對(duì)象,里面放的就是要記錄的最小溫度、最大溫度、總溫度和總數(shù)。
圖片
整個(gè)核心代碼就二三十行,使用了流式編程:
圖片
首先是一行行的讀取文本,接著每一行都按照分號(hào)進(jìn)行拆分,取出對(duì)應(yīng)的氣象站和溫度值。
然后按照氣象站維度進(jìn)行 groupingBy 聚合,并且計(jì)算最大值、最小值和平均值。
在計(jì)算平均值的時(shí)候,為了避免浮點(diǎn)計(jì)算,還特意將溫度乘 10,轉(zhuǎn)換為 int 類型。
最后用 TreeMap 按字典序輸出各個(gè)氣象站的溫度數(shù)據(jù)。
這個(gè)基線版本官方的數(shù)據(jù)是在跑分環(huán)境下,2 分鐘內(nèi)可以運(yùn)行完畢。
而在我的電腦上跑了接近 14 分鐘:
很正常,畢竟人家的測(cè)評(píng)環(huán)境配置都是很高的:
Results are determined by running the program on a Hetzner AX161 dedicated server (32 core AMD EPYC? 7502P (Zen2), 128 GB RAM).
參加挑戰(zhàn)的各路大神,最終拿出的 TOP 10 成績(jī)是這樣的:
圖片
當(dāng)時(shí)看到這個(gè)成績(jī)的瞬間,我人都是麻的,第一個(gè)疑問(wèn)是:我靠,13G 的文件啊?1.5s 內(nèi)完成了讀取、解析、計(jì)算的過(guò)程?這不可能啊,光是讀取 13G 大小的文件,也需要一點(diǎn)時(shí)間吧?
但是需要注意的是,歪師傅有這個(gè)想法是走入了一個(gè)小誤區(qū),就是我以為這 13G 的文件一次性加載不完成,怎么快速的從硬盤把文件讀取到內(nèi)存中也是一個(gè)考點(diǎn)。
后來(lái)發(fā)現(xiàn)是我多慮了,人家直接就說(shuō)了,不用考慮這一點(diǎn),跑分成績(jī)運(yùn)行的時(shí)候,文件直接就在內(nèi)存中:
圖片
所以,最終的成績(jī)中不包含讀取文件的時(shí)間。
但是也很牛逼了啊,畢竟有一億條數(shù)據(jù)。
第一名
我嘗試著看了一下第一名的代碼:
過(guò)于硬核,實(shí)在是看不懂。我只能通過(guò)作者寫(xiě)的一點(diǎn)注釋、方法名稱、代碼提交記錄去嘗試?yán)斫馑拇a。
在他的代碼開(kāi)頭的部分,有這樣的一段描述:
圖片
這是他的破題思路,結(jié)合了這些信息之后再去看代碼,稍微好一點(diǎn),但是我發(fā)現(xiàn)他里面還是有非常多的微操、太多針對(duì)性的優(yōu)化導(dǎo)致代碼可讀性較差,雖然他的代碼加上注釋一共也才 400 多行,然而我看還是看不懂。
我隨便截個(gè)代碼片段吧:
圖片
問(wèn) GPT 這個(gè)哥們,他也是能說(shuō)個(gè)大概出來(lái):
圖片
所以我放棄了理解第一名的代碼,開(kāi)始去看第二名,發(fā)現(xiàn)也是非常的晦澀難懂,再到第三名...
最后,我產(chǎn)生了文章開(kāi)始時(shí)的疑問(wèn):他們寫(xiě)的 Java 和我會(huì)的 Java 是同一個(gè) Java 嗎?
但是有一說(shuō)一,雖然我看不懂他們的某些操作,但是會(huì)發(fā)現(xiàn)他們整體的思路都幾乎是一致。
雖然我沒(méi)有看懂第一名的代碼,但是我還是專門列出了這一個(gè)小節(jié),給你指?jìng)€(gè)路,有興趣你可以去看看。
另外,獲得第一名的老哥,其實(shí)是一個(gè)巨佬:
圖片
是 GraalVM 項(xiàng)目的負(fù)責(zé)人之一:
圖片
巨人肩膀
在官方的 github 項(xiàng)目的最后,有這樣的一個(gè)部分:
圖片
其中最后一篇文章,是一個(gè)叫做 Marko Topolnik 的老哥寫(xiě)的。
我看了一下,這個(gè)哥們的官方成績(jī)是 2.332 秒,榜單第九名:
但是按照他自己的描述,在比賽結(jié)束后他還繼續(xù)優(yōu)化了代碼,最終可以跑到 1.7s,排名第四。
在他的文章中詳細(xì)的描述了他的挑戰(zhàn)過(guò)程和思路。
我就站在巨人的肩膀上,帶大家看看這位大佬從 71s 到 1.7s 的破題之道:
https://questdb.io/blog/billion-row-challenge-step-by-step/
最常規(guī)的代碼
首先,他給了一個(gè)常規(guī)實(shí)現(xiàn)的代碼,和基線版本的代碼大同小異,只不過(guò)是使用了并行流來(lái)處理:
https://github.com/mtopolnik/billion-row-challenge/blob/main/src/Blog1.java
圖片
平時(shí)看到流式編程我是有點(diǎn)頭疼的,需要稍微的反應(yīng)一下,但是在看了前三名的最終代碼后再看這個(gè)代碼,我覺(jué)得很親切。
根據(jù)作者的描述,這段代碼:
- 使用并行 Java 流,將所有 CPU 核心都用起來(lái)了。
- 也不會(huì)陷入任何已知的性能陷阱,比如 Java 正則表達(dá)式
在一臺(tái)裝有 OpenJDK 21.0.2 的 Hetzner CCX33 機(jī)器上,跑完需要的時(shí)間為 71 秒。
第 0 版優(yōu)化:換個(gè)好的 JVM
叫做第 0 版優(yōu)化的原因是作者對(duì)于代碼其實(shí)啥也沒(méi)動(dòng),只是換了一個(gè) JVM:
圖片
默認(rèn)使用 GraalVM 之后,最常規(guī)的代碼,運(yùn)行時(shí)間從 71s 到了 66s,相當(dāng)于白撿了 5s,我問(wèn)就你香不香。
同時(shí)作者還提到一句話:
When we get deeper into optimizing and bring down the runtime to 2-3 seconds, eliminating the JVM startup provides another 150-200ms in relief. That becomes a big deal.
當(dāng)我們把程序優(yōu)化到運(yùn)行時(shí)間只需要 2-3 秒的時(shí)候,使用 GraalVM,會(huì)消除 JVM 的啟動(dòng)時(shí)間,從而提供額外的 150-200ms 的提升。
到那個(gè)時(shí)候,這個(gè)就變得非常重要了。
數(shù)據(jù)指標(biāo)很重要
在正式進(jìn)入優(yōu)化之前,作者先介紹了他使用到的三個(gè)非常重要的工具:
圖片
關(guān)于工具我就不過(guò)多介紹了,這里單獨(dú)提一嘴主要是想表達(dá)一個(gè)貫穿整個(gè)優(yōu)化過(guò)程的中心思想:數(shù)據(jù)指標(biāo)很重要。
你只有收集到了當(dāng)前程序足夠多的運(yùn)行指標(biāo),才能對(duì)你進(jìn)行下一步優(yōu)化時(shí)提供直觀的、優(yōu)化方向上的指導(dǎo)。
工欲善其事必先利其器,就是這個(gè)道理。
第一版優(yōu)化:并行 I/O 搞起來(lái)
通過(guò)查看當(dāng)前代碼對(duì)應(yīng)的火焰圖:https://questdb.io/html/blog/profile-blog1
通過(guò)火焰圖以及觀察 GC 情況,作者發(fā)現(xiàn)當(dāng)前耗時(shí)的地方注意是這三個(gè)地方:
圖片
- BufferedReader 將每行文本輸出為字符串
- 處理每一行的字符串
- 垃圾收集 (GC):使用 VisualGC 可以看到,差不多每秒要 GC 10 次甚至更多。
可以發(fā)現(xiàn) BufferedReader 占用了大量的性能,因?yàn)楫?dāng)前讀取文件還是一行行讀取的嘛,性能很差。
于是大多數(shù)人意識(shí)到的第一件事就是采用并行化 I/O。
所以,我們需要把待處理的文件分塊。分多少塊呢?
有多少個(gè)線程就分成多少個(gè)塊,每個(gè)線程各自處理一個(gè)塊,這樣性能就上去了。
文件分塊讀取,大家自然而然的就想到了 mmap 相關(guān)的方法。
mmap 可以用 ByteBuffer API 來(lái)搞事情,但是使用的索引是 int 類型,所以可映射的大小有 2GB 的限制。
前面說(shuō)了,在這個(gè)挑戰(zhàn)中,光是文件大小就有 13G,所以 2GB 是捉襟見(jiàn)肘的。
但是在 JDK 21 中,支持一個(gè)叫做 MemorySegment 的東西,也可以干 mmap 一樣的事情,但是它的索引使用的是 long,相當(dāng)于沒(méi)有內(nèi)存限制了。
除了使用 MemorySegment 外,還有一些細(xì)節(jié)的處理,比如找到正確的分割文件的位置、啟動(dòng)線程、等待線程處理完成等等。
處理這些細(xì)節(jié)會(huì)導(dǎo)致這一版的代碼從最初的 17 行增加到了 120 行。
這是優(yōu)化后的代碼地址:
https://github.com/mtopolnik/billion-row-challenge/blob/main/src/Blog2.java
在這個(gè)賽題下,我們肯定是需要再循環(huán)中進(jìn)行數(shù)據(jù)的解析和處理的,所以循環(huán)就是非常重要的一個(gè)點(diǎn)。
我們可以關(guān)注一下代碼中的循環(huán)部分,這里面有一個(gè)小細(xì)節(jié):
這個(gè)循環(huán)是每個(gè)線程在按塊讀取文件大小,里面用到了 findByte 方法和 stringAt 方法。
在第一個(gè)版本中,我們是用的 BufferedReader 把一行內(nèi)容以字符串的形式讀進(jìn)來(lái),然后按照分號(hào)分隔,并生成城市和溫度兩個(gè)字符串。
這個(gè)過(guò)程就涉及到三個(gè)字符串了。
但是這個(gè)哥們的思路是啥?
自定義一個(gè) findByte 方法,先找到分號(hào)的位置,然后把下標(biāo)返回回去。
再用自定義的 stringAt 方法,結(jié)合前面找到的下標(biāo),直接解析出“城市和溫度”這兩個(gè)字符串,減少了整行讀取的內(nèi)存消耗。
相當(dāng)于少了一億個(gè)字符串,在字符串處理和 GC 方面取得了不錯(cuò)的表現(xiàn)。
這一波操作下來(lái),處理時(shí)間直接從 66s 下降到了 17s:
圖片
然后再看火焰圖:
圖片
可以發(fā)現(xiàn) GC 的時(shí)間幾乎消失了。
CPU 現(xiàn)在大部分時(shí)間都花在自定義的 stringAt 上。還有相當(dāng)多的時(shí)間花在 Map.computeIfAbsent 方法 、Double.parseDouble 方法和 findByte 方法
其中 Double.parseDouble 方法是解析溫度用的。
作者打算先把這個(gè)地方給攻下來(lái)。
第二版優(yōu)化:優(yōu)化溫度解析方法
在這版優(yōu)化中,作者直接將溫度解析為整數(shù)。
首先,目前的做法是,首先分配一個(gè)字符串,然后對(duì)其調(diào)用 parseDouble() 方法,最后轉(zhuǎn)換為整數(shù)以進(jìn)行高效的存儲(chǔ)和計(jì)算。
但是,其實(shí)我們應(yīng)該直接創(chuàng)建整數(shù)出來(lái),沒(méi)必要走字符串繞一圈。
同時(shí)我們知道,溫度的取值范圍是 [-99.9,99.9],所以針對(duì)這個(gè)范圍,我們搞個(gè)自定義方法就行了:
private int parseTemperature(long semicolonPos) {
long off = semicolonPos + 1;
int sign = 1;
byte b = chunk.get(JAVA_BYTE, off++);
if (b == '-') {
sign = -1;
b = chunk.get(JAVA_BYTE, off++);
}
int temp = b - '0';
b = chunk.get(JAVA_BYTE, off++);
if (b != '.') {
temp = 10 * temp + b - '0';
// we found two integer digits. The next char is definitely '.', skip it:
off++;
}
b = chunk.get(JAVA_BYTE, off);
temp = 10 * temp + b - '0';
return sign * temp;
}
這波操作下來(lái),處理時(shí)間又減少了 6s,來(lái)到了 11s:
圖片
再看對(duì)應(yīng)火焰圖:
圖片
溫度解析部分的耗時(shí)占比從 21.43% 降低到 6%,說(shuō)明是一次正確的優(yōu)化。
接下來(lái),可以再搞一搞 stringAt 方法了。
第三版優(yōu)化:自定義哈希表
首先,要優(yōu)化 stringAt 方法,我們得知道它是干啥的。
我們看一眼代碼:
圖片
在經(jīng)歷了上一波優(yōu)化之后,stringAt 目前在代碼中的唯一作用就是為了獲取氣象站的名稱。
而獲取到這個(gè)名稱的唯一目的是看看當(dāng)前的 HashMap 中有沒(méi)有這個(gè)氣象站的數(shù)據(jù),如果沒(méi)有就新建一個(gè) StationStats 對(duì)象,如果有就把之前的 StationStats 對(duì)象拿出來(lái)進(jìn)行數(shù)據(jù)維護(hù)。
此外,在賽題中還有這樣的一個(gè)信息,雖然有一億行數(shù)據(jù),但是只有 413 個(gè)氣象站:
圖片
既然 key 的大小是可控的,那基于這個(gè)條件,作者想了一個(gè)什么樣的騷操作呢?
他直接不用 HashMap 了,自定義了一個(gè)哈希表,長(zhǎng)這樣的:
https://github.com/mtopolnik/billion-row-challenge/blob/main/src/Blog3.java
圖片
主要看一下代碼中的 findAcc 方法,你就能明白它是干啥的了:
圖片
通過(guò) hash 方法計(jì)算出指定字符串,即氣象站名稱的 hash 值之后,從自定義的 hashtable 中取出該位置的數(shù)據(jù)。
首先標(biāo)號(hào)為 ① 的地方,如果沒(méi)有取到數(shù)據(jù),則說(shuō)明沒(méi)有這個(gè)氣象站的數(shù)據(jù),新建一個(gè)放好,返回就完事。
如果取到了數(shù)據(jù),來(lái)到標(biāo)號(hào)為 ② 的地方,看看取到的數(shù)據(jù)和當(dāng)前要放的數(shù)據(jù)對(duì)應(yīng)的氣象站名稱是不是一樣的。
如果是則說(shuō)明已經(jīng)有了,取出來(lái),然后返回。
如果不是,說(shuō)明啥情況?
說(shuō)明 hash 沖突了,來(lái)到標(biāo)號(hào)為 ③ 的地方進(jìn)行下標(biāo)加一的動(dòng)作。
然后再次進(jìn)行循環(huán)。
來(lái),你告訴我,這是什么手法?
這不就是開(kāi)放尋址來(lái)解決 hash 沖突嗎?
所以 findAcc 方法,就可以替代 computeIfAbsent 方法。
通過(guò)自定義的 StatsAcc 哈希表來(lái)代替原生的 HashMap。
而且前面說(shuō)了,key 的大小是可控的,如果自定義 hash 表的初始化大小控制的合適,那么整個(gè) hash 沖突的情況也不會(huì)非常嚴(yán)重。
這一波組合拳下來(lái),運(yùn)行時(shí)間來(lái)到了 6.6s,火焰圖變成了這樣:
圖片
大量的時(shí)間花在了前面分析的 findAcc 方法上。
同時(shí)作者提到了這樣一句話:
圖片
同樣的代碼,如果放到 OpenJDK 上跑需要運(yùn)行 9.6s,比 GraalVM 慢了 3.3s。
我滴個(gè)乖乖,這就是一個(gè) 45% 的性能提升啊。
第四版優(yōu)化:使用 Unsafe 和 SWAR
在這一版優(yōu)化開(kāi)始之前,作者先寫(xiě)了這樣一段話:
圖片
大概意思就是說(shuō),到目前為止,我們用到的都是常規(guī)且有效的解決方案,并且是 Java 標(biāo)準(zhǔn)、安全的用法。
即使止步于此也能學(xué)到很多優(yōu)化技巧,可以在實(shí)際的項(xiàng)目中進(jìn)行使用。
如果你繼續(xù)往下探索,那么:
Readability and maintainability also take a big hit, while providing diminishing returns in performance. But, a challenge is a challenge, and the contestants pressed on without looking back!
可讀性和可維護(hù)性也會(huì)受到重創(chuàng),同時(shí)性能的收益會(huì)遞減。但是,挑戰(zhàn)就是挑戰(zhàn),參賽者們繼續(xù)努力,沒(méi)有回頭!
簡(jiǎn)單來(lái)說(shuō),作者的意思就是打個(gè)預(yù)防針:接下來(lái)就要開(kāi)始上強(qiáng)度了。
所以,在這個(gè)版本中,作者應(yīng)用一些排名靠前的選上都在用的方案:
- 使用 sun.misc.Unsafe 而不是 MemorySegment,來(lái)避免邊界檢查
- 避免重新讀取相同的輸入字節(jié):重復(fù)使用加載的值進(jìn)行哈希和分號(hào)搜索
- 每次處理 8 個(gè)字節(jié)的數(shù)據(jù),使用 SWAR 技術(shù)找到分號(hào)分隔符。
- 使用 merykitty 老哥提供的牛逼的 SWAR(SIMD Within A Register)代碼解析溫度。
這是這一版的代碼:https://github.com/mtopolnik/billion-row-challenge/blob/main/src/Blog4.java
比如其中關(guān)于循環(huán)處理數(shù)據(jù)的部分,看起來(lái)就很之前很不一樣了:
圖片
然后你再看里面 semicolonMatchBits、nameLen、maskWord、dotPos、parseTemperature 這些方法的調(diào)用,直接就是一個(gè)懵逼的狀態(tài),看著頭都大了:
圖片
但是你仔細(xì)看,你會(huì)發(fā)現(xiàn)這幾個(gè)方法是作者從其他人那邊學(xué)來(lái)的:
圖片
比如這個(gè)叫做 merykitty 的老哥,提供了解析溫度的代碼,雖然作者加入了大量的注釋說(shuō)明,但是我也只是大概就看懂了不到三層吧。
這里面大量的使用了位運(yùn)算的技巧,同時(shí)你仔細(xì)看:幾乎沒(méi)有 if 判斷的存在。這是重點(diǎn),用直接的位運(yùn)算替換了分支指令,從而減少了分支預(yù)測(cè)錯(cuò)誤的成本。
此外,還有很多我第一次見(jiàn)、叫不上名字的奇技淫巧。
通過(guò)這一波“我看不懂,但是我大受震撼”的操作搞下來(lái),時(shí)間降低到了 2.4s:
圖片
第五版優(yōu)化:統(tǒng)計(jì)學(xué)用起來(lái)
現(xiàn)在,我們的火焰圖變成了這樣:https://questdb.io/html/blog/profile-blog4
圖片
耗時(shí)主要還是在于 findAcc 方法:
圖片
而 findAcc 方法的耗時(shí)在于 nameEquals 方法,判斷當(dāng)前氣象站名稱是否出現(xiàn)過(guò):
圖片
但是這個(gè)方法里面有個(gè) if 判斷,以字節(jié)為單位比較兩個(gè)字符串的內(nèi)容,每次比較 8 個(gè)字節(jié)。
首先,它通過(guò)循環(huán)逐步比較兩個(gè)字符串中的對(duì)應(yīng)字節(jié)。在每次迭代中,它使用 getLong 方法從輸入字符串中獲取一個(gè) 64 位的長(zhǎng)整型值,并與另一個(gè)字符串中的相應(yīng)位置進(jìn)行比較。如果發(fā)現(xiàn)不相等的字節(jié),則返回 false,表示兩個(gè)字符串不相等。
如果循環(huán)結(jié)束后沒(méi)有發(fā)現(xiàn)不相等的字節(jié),它會(huì)繼續(xù)檢查是否已經(jīng)比較了輸入字符串的所有字節(jié),或者最后一個(gè)輸入字符串的字節(jié)與相應(yīng)位置的字符串字節(jié)相等,那么表示兩個(gè)字符串相等,則返回 true。
那么問(wèn)題就來(lái)了?
如果氣象站名稱長(zhǎng)度全都是小于 8 個(gè)字節(jié),會(huì)出現(xiàn)啥情況?
假設(shè)有這樣的一個(gè)前提條件,是不是我們就不用在 for 循環(huán)中進(jìn)行 if 判斷了,直接一把就比較完成了?
很可惜,沒(méi)有這樣一個(gè)提前條件。
但是,如果在數(shù)據(jù)集中,氣象站名稱長(zhǎng)度絕大部分都小于 8 個(gè)字節(jié)那是不是就可以單獨(dú)處理一下?
那到底數(shù)據(jù)分布是怎么樣的呢?
這個(gè)問(wèn)題問(wèn)題出去的一瞬間,統(tǒng)計(jì)學(xué)啪的一下就站出來(lái)了:這個(gè)老子在行,我算算。
所以,作者寫(xiě)了一個(gè)程序來(lái)統(tǒng)計(jì)分析數(shù)據(jù)集中氣象站名稱的長(zhǎng)度:https://github.com/mtopolnik/billion-row-challenge/blob/main/src/Statistics.java
基于程序運(yùn)行結(jié)果,最終的結(jié)論如下:
圖片
圖片
通過(guò)分析作者發(fā)現(xiàn),賽題的數(shù)據(jù)集中氣象站名稱長(zhǎng)度幾乎均勻分布在 8 字節(jié)以上和 8 字節(jié)以下。
運(yùn)行 Statistics.branchPrediction 方法,當(dāng)條件是 nameLen > 8 時(shí)導(dǎo)致了 50% 的分支預(yù)測(cè)失敗。
也就是說(shuō),一億數(shù)據(jù)中有一半的數(shù)據(jù),都是小于 8 字節(jié)的,都是不用特意進(jìn)行 if 判斷的。
但如果將條件更改為 nameLen > 16,那么預(yù)測(cè)失敗率將降至 2.5%。
根據(jù)這一發(fā)現(xiàn),很明顯,如果要進(jìn)一步優(yōu)化代碼,就需要編寫(xiě)一些特定的代碼來(lái)避免在 nameLen > 8 上使用任何 if 判斷,直接使用 nameLen > 16 就行。
這是這一版的最終代碼,可讀性越來(lái)越差了:https://github.com/mtopolnik/billion-row-challenge/blob/main/src/Blog5.java
但是最終的成績(jī)是 1.8s:
圖片
哦,對(duì)了,如果你對(duì)于分支預(yù)測(cè)技術(shù)不太清楚,那你可能看得比較懵。
但是分支預(yù)測(cè),在性能挑戰(zhàn)中,特別是最后大家比分都咬的非常緊的情況下,每次都是屢立奇功,戰(zhàn)功赫赫,屬于高手間過(guò)招殺手锏級(jí)別的優(yōu)化手段。
繼續(xù)優(yōu)化
再后面作者還有這兩個(gè)部分。
消除啟動(dòng)/清理成本:
圖片
使用更小的文件分塊和工作竊取機(jī)制:
圖片
這后面就完全是基于這個(gè)賽題進(jìn)行定制化的優(yōu)化,可移植性不強(qiáng)了,作者就沒(méi)有進(jìn)行詳細(xì)描述,再加上一個(gè)我也是沒(méi)怎么看明白,就不展開(kāi)講了。
反正這兩個(gè)組合拳下來(lái),又搞了 0.1s 的時(shí)間下來(lái),最終的成績(jī)?yōu)?1.7s:
圖片
我實(shí)在是學(xué)不動(dòng)了,有興趣的同學(xué)可以自己去看看原文的對(duì)應(yīng)部分。