Java社群的一次十億行資料程式設計挑戰

Gunnar Morling 是一位軟體工程師和開源愛好者,目前在 Decodable 從事基於 Apache Flink 的流處理工作。之前他在 Redhat 領導了 Debezium 專案。他是 Java Champion,創立了多個開源專案,如 JfrUnit、kcctl 和 MapStruct。Gunnar 曾在 QCon、Java One 和 Devoxx 等各種會議上發表過演講。本文中,Gunnar Morling 討論了應對一個包含十億行資料的檔案時,速度最快的解決方案所採用的一些技巧,這些技巧透過並行化和高效的記憶體訪問在不到兩秒的時間內就能處理 13 GB 的輸入檔案。
我想談談我之前參加的一個病毒式程式設計挑戰,名為“十億行挑戰”(1 Billion Row Challenge,1BRC)。
我在一家名為 Decodable 的公司擔任軟體工程師。我們基於 Apache Flink 構建了一個用於流處理的託管平臺。這件事對我來說完全是副業,但 Decodable 支援我這樣做。
之所以舉辦這項挑戰是因為我想學點新東西。每六個月就會有新的 Java 版本出現,帶有新的 API 和新功能。要跟蹤所有這些開發成果並非易事。我想知道這些新 Java 版本中有哪些新東西,我能用它們做什麼?同時我也想為社群提供一個渠道讓大家都能學到新東西。在這個挑戰中,你可以從其他人的實現裡汲取靈感。另外我還想要糾正一個偏見,那就是很多人認為 Java 很慢。但如果你看看現代版本及其特性,你會發現這完全是錯誤的。
挑戰內容
我們先來深入研究一下挑戰的細節。
我們的想法是,有一個包含溫度測量值的檔案,本質上就像一個 CSV,但它不是以逗號,而是以分號作為分隔符。內容有兩列,還有一個車站名稱,如漢堡或慕尼黑等。與之關聯的溫度測量值是隨機值。
挑戰任務是處理該檔案,彙總檔案中的值,併為每個站點確定最小值、最大值和平均溫度值,很簡單。唯一的警告是,正如挑戰的名稱所示,它有 10 億行,生成檔案的大小約為 13 GB,相當大。然後你必須打印出結果,如上圖。
規則
關於規則再多說一點。首先,這主要針對 Java。為什麼?因為這是我最瞭解的平臺,我想支援它,想傳播關於它的知識。然後,你可以選擇任何版本,新版本、預覽版本、所有型別的發行版,如 GraalVM,或各種各樣的 JDK 提供程式都可以。你可以使用一個名為 SDKMAN 的工具進行管理。這是一個非常好的工具,可以管理很多 Java 版本,並在不同版本之間來回切換。
僅限 Java,沒有依賴項。引入一些庫為你完成任務沒什麼意義,你應該自己程式設計。各次執行之間沒有快取。我對每個實現都運行了五次。然後我丟棄了最快和最慢的那個,並從剩下的三次執行結果中取平均值。可以快取的話,你只需執行一次任務,將結果儲存在檔案中,然後將其讀回,這樣就會非常快。這沒有多大意義。你也可以從他人那裡獲得靈感。
評估環境
關於我的執行環境:
我的公司花了 100 歐元購買了這臺機器,有 32 個核心,其中我主要只使用 8 個核心。有相當多的 RAM。實際上檔案總是從 RAM 盤讀取。我想確保磁碟 I/O 不至於影響效能,因為它的可預測性要差得多。這裡只有一個純粹的 CPU 繫結問題。
上面是我的基本實現。我使用這個 Java Streams API、這個 files.lines 方法,它為我提供了一個包含檔案行的流。我從磁碟讀取該檔案,然後使用 split 方法將每行對映到那裡。我想將站名與值分開。然後我將結果(行)收集到這個分組收集器中。我按站名對其分組。
然後,對於我的每個站,我需要聚合這些值,在我的聚合器實現中處理。每當新值新增到現有聚合器物件時,我都會跟蹤最小值和最大值。為了計算平均值,我會跟蹤值的總和和計數。非常簡單。然後,如果我並行執行這個操作,我需要合併兩個聚合器,同樣非常簡單。最後,如果我完成了,需要減少處理結果,然後透過物件發出這樣的結果,其中包含最小值和最大值。對於平均值,我只需將總數除以計數,然後將其打印出來。在這臺機器上這大約需要五分鐘,不算超級快,但也不是很糟糕。編寫這段程式碼花了我半個小時不到,還不錯。如果你在工作中解決這個問題,到這裡你可能會收工回家,喝杯咖啡,就完事了。當然,為了這次挑戰的目的,我們希望速度更快,看看我們能在這裡取得多大的進展。
第一個提交
這項挑戰重要的是必須有人來參與。一位來自荷蘭的 Java 冠軍 Roy Van Rijn 立刻對此產生了興趣,在我釋出帖子後大約一小時,他建立了自己的第一個實現,不是很花哨或很複雜。只要第一個人出現,其他人也會來參與。
並行化
我們深入瞭解一下如何加快這個程式的速度。人們花了整個一月份的時間來研究這個問題,他們探索到了一個非常深的層次,基本上是計算 CPU 指令。
首先我們談談並行化,因為我們有很多 CPU 核心。在我用來評估它的伺服器上有 32 個核心,64 個執行緒。我們想利用這一點,只使用一個核心會有點浪費。我們該怎麼做呢?回到我簡單的基線實現,我能做的第一件事就是新增這個並行呼叫,也就是 Java Streams API 的這一部分。
現在它將並行處理這個管道,或者說這個流管道的一部分。只需新增這個單一方法呼叫,就可以將時間縮短到 71 秒,非常輕鬆的勝利。
如果你仔細想想,是的,它讓我們的速度加快了不少,但並沒有達到八倍的水平。可我們有 8 個 CPU 核心,為什麼它沒有八倍的速度?因為這個並行運算子適用於處理邏輯。所有這些聚合和分組邏輯都是並行發生的,但從記憶體中讀取檔案仍然是按順序發生的。讀取部分是按順序進行,其他 CPU 核心依舊處於空閒狀態,所以我們也想將其並行化。
新的 Java 版本都帶有新的 API、JEP、Java 增強提案。其中之一是最近新增的外部函式和記憶體 API。
本質上它是一個 Java API,允許你使用原生方法。它比舊的 JNI API 更易用,還允許你使用本機記憶體。你可以管理自己的記憶體部分(如堆外記憶體),而不是由 JVM 管理的堆,並且你將負責維護它、釋放它,等等。我們想在這裡使用它,因為我們可以記憶體對映這個檔案,然後在那裡並行處理它。
首先我們確定並行度。我們的例子中是八個核心,這就是我們的並行度。接下來我們要對這個檔案進行記憶體對映。在早期的 Java 版本中,你也可以使用記憶體對映檔案,但你有大小之類的限制,你無法一次對整個 13 GB 的檔案進行記憶體對映。而現在有了新的外部記憶體 API,我們就可以做到這一點。你對映檔案。我們有這個 Arena 物件。這本質上是我們對這個記憶體的表示。有不同型別的 Arena,這裡我只是使用這個全域性 Arena,它可以從我的應用程式中的任何位置訪問。現在我可以使用多個執行緒並行訪問整個記憶體部分。
為了做到這一點,我們需要分割檔案和記憶體表示。首先,我們將其大致分成八個相等的塊。我們將整個大小除以八。當然,很有可能我們會分割到某一行的中間,而理想情況下,我們希望我們的工作程序能夠處理整行。這裡發生的事情是,我們轉到了檔案的大約八分之一,然後繼續轉到下一個行結束符。那麼這裡就是這個塊的結尾,也是下一個塊的起點。然後我們處理這些塊,我們啟動執行緒,然後將它們連線起來。現在我們真正在整個週期內都利用了所有 8 個核心,進行 I/O 時也一樣。
有一個警告。從本質上講,其中一個 CPU 核心總是最慢的。在某個時候,其他七個核心都會等待最後一個核心完成,因為資料的分佈有點不均勻。人們最終的做法是不再使用 8 個塊,而是將這個檔案分成更小的塊。本質上,他們積壓了這些塊。每當其中一個工作執行緒處理完當前塊時,它就會去處理下一個塊。透過這種方式,你可以確保所有 8 個執行緒始終得到平等利用。事實證明,理想的塊大小是 2 兆位元組。為什麼是 2 兆位元組?我使用的這臺機器上的這個 CPU 有 16 兆位元組的二級快取,8 個執行緒每次處理 2 兆位元組。這個數值在預測 I/O 等方面是最好的。這也表明,我們確實深入到了具體的 CPU 和架構的層面來真正最佳化該問題。
解   析
我們更深入地分析一下。我們已經瞭解瞭如何利用多個 CPU 核心,但具體處理每一行時究竟發生了什麼?我們仔細看看。
我們想擺脫最初的使用正則表示式等分割檔案的做法,那樣效率並不高。我能想到的辦法是,只需逐個字元地處理這些輸入行即可。
這裡差不多是一個狀態機。我們讀取字元,繼續讀取行,直到沒有字元。然後我們使用將站點名稱與溫度值分隔開的分號字元來切換這些狀態。根據我們所處的狀態,我們是讀取組成站點名稱的位元組,還是讀取組成測量值的位元組?我們需要將它們新增到某個構建器或緩衝區中,以聚合這些值。
然後,如果我們在一行的末尾,也就是說我們找到了行結束符,那麼我們需要使用建立的這兩個緩衝區來記錄站點和測量值。對於測量值,我們需要了解如何將其轉換為整數值,這也是人們想出的辦法。這個問題被描述為雙精度或浮點運算,因此值是 21.7 度,但同樣,我總是隻遇到一個小數位。人們意識到,這個資料實際上總是隻有一個小數位。我們可以利用這一點,只需將數字乘以 100 即可將其視為整數問題,作為計算方法。然後在最後,將其除以 100 或 10。這是人們經常做的事情,我低估了他們會在多大程度上利用該資料集的特定特性。
所以我們處理或使用這些值。如果我們看到減號就取反這個值。如果我們看到兩位數字中的第一個,就把它乘以 100 或 10。這樣做,我們可以把時間縮短到 20 秒,已經比最初的基線實現快了一個數量級。
到目前為止沒有什麼真正神奇的事情。你也應該得到一個啟示,繼續做這樣的事情有多大意義?如果這是你在日常工作中面臨的問題,也許就此打住吧。它可讀性好,可維護性好。它比原生基線實現快了一個數量級,所以這相當不錯。
當然,為了應對這一挑戰,我們可能需要走得更遠。我們還能做什麼?我們可以再次回到並行的概念,嘗試一次處理多個值,現在我們有了不同的並行方法。我們已經看到了如何充分利用所有 CPU 核心。這是並行度的一方面。我們還可以考慮擴充套件到多個計算節點,這通常是我們對大規模資料儲存所做的事情。對於這個問題它並不那麼重要,我們必須拆分該檔案並將其分發到網路中。也許不是那麼理想,但那將是另一個極端。而我們也可以朝另一個方向發展,在特定的 CPU 指令內作並行化。這就是 SIMD(單指令多資料)所做的事情。
基本上所有這些 CPU 都有擴充套件指令,允許你一次將相同型別的操作應用於多個值。例如,在這裡我們想找到行尾字元。現在我們不再逐位元組對比,而是可以使用這樣的 SIMD 指令將其應用於 8 個或 16 個甚至更多位元組,當然這會大大加快速度。問題是,在 Java 中,你沒有很好的方法來利用這些 SIMD 指令,因為它是一種可移植的抽象語言,它不允許你降低到這種級別的 CPU 底層上。
好訊息是我們可以用上面這個向量 API,它仍在孵化中,在第八個孵化版本左右。這個 API 現在允許你在擴充套件中使用這些向量化指令。你可以使用這個相等運算子進行類似這樣的比較呼叫,然後它將被轉換為底層架構的正確 SIMD 指令,轉換為 Intel 或 AMD64 擴充套件。對於 Arm,它也會這樣做。如果你的機器沒有任何向量擴充套件,它將回退到標量執行。這是指令級別的並行化。我對此作了另一次演講(https://speakerdeck.com/gunnarmorling/to-the-moon-and-beyond-with-java-17-apis),其中向你展示瞭如何使用 SIMD 解決 FizzBuzz。
這種模式可以一次將相同的操作應用於多個值,此外我們還可以執行所謂的 SWAR,即暫存器內的 SIMD。
這裡的想法是,做同樣的事情,比如一次處理多個值的相等操作,我們也可以在單個變數中執行此操作。如果你有 8 個位元組,我們也可以看到一個 long,那是 64 位,那也是 8 個位元組。我們可以將正確級別的位級魔法應用於 long 值,然後將此操作應用於所有 8 個位元組。這裡會有位級遮蔽和移位等等事情。Richard Startin 有一篇非常好的部落格文章,一步一步地向你展示瞭如何做到這一點,或者如何使用它來查詢字串中的第一個零位元組。
我把數學公式放在右邊,你會看到,這實際上給了你一個長整型的第一個零位元組。這就是暫存器內的 SIMD,SWAR。現在有趣的是,如果你看一下這段程式碼,會發現這裡缺少了一些東西。有人意識到我們這裡缺了什麼嗎?這段程式碼中沒有 if、沒有條件、沒有分支。這實際上非常重要,因為我們需要記住 CPU 實際上是如何工作的。如果你看看 CPU 如何獲取和執行我們的程式碼,會發現它總是採用這種流水線方法。每條指令都有這個階段,它將從記憶體中獲取,解碼,執行,最後寫回結果。現在這些事情中的多個操作是並行發生的。當我們解碼一條指令時,CPU 已經去獲取下一條指令了。這是一種流水線並行化方法。
當然,CPU 實際上需要知道下一條指令是什麼,否則我們就不知道要獲取什麼。為了讓它知道,我們實際上不能有任何 if,因為那樣我們就不知道要往哪個方向走。如果你有一種用這種無分支方式表達這個問題的方法(就像我們之前看到的那樣),那麼這對 CPU 中分支預測器來說非常有益,這樣它就總能知道下一條指令是什麼。我們從來沒有遇到過一種情況,就是實際上需要重新整理這個管道,只因為我們在這個預測執行中走了一條錯誤的路徑。
人們經常使用的資源之一是這本書《駭客的喜悅》。如果你對此感興趣,我建議每個人都去買這本書。比如這個問題,比如在字串中找到第一個零位元組,所有這些演算法、例程都在這本書中描述過了。如果這件事讓你興奮,一定要看看這本書買下來。
然後災難發生了
有一天我醒來,突然間所有這些結果都不一樣了。它比以前快了兩倍。我運行了前一天執行過的一個實現,突然間速度快了很多。我在想發生了什麼事?
其實這個負載最初是在虛擬伺服器上跑的。我得到了專用的虛擬 CPU 核心,這樣我就不會在那臺機器上有任何吵鬧的鄰居了。我沒想到的是他們會直接把這個負載轉移到另一臺機器上。我不知道為什麼會這樣。也許原因是隨機的。也許他們看到了那臺機器上有很多負載。無論如何,它只是被轉移到了另一臺比以前更快的主機上。這當然對我來說是一個大問題,因為到目前為止我所做的所有對比都是錯誤的,不再具有可比性。我意識到自己需要一臺專用伺服器,正如我提到的,我的僱主 Decodable 挺身而出贊助了它。
當然我還需要維護方面的幫助,因為我不是運維方面的專家。例如,你可能想關閉超執行緒,或者你想關閉核心加速功能以獲得穩定的結果。我並不擅長做這些事情,但社群的 René 來幫忙了。他主動提出幫助設定這個東西。有很多這樣的人都來幫忙,然後人們實際上構建了一個 TCK,一個測試套件。
它實際上是一個你必須透過的測試套件。你不僅想要快,還想要正確。人們構建了這個測試套件,它實際上隨著時間的推移而增長。然後,每當有新的提交、新的條目進來時,它首先必須透過這些測試。然後如果它有效,那麼我會去評估它並執行實現。這就是它的樣子。
它有帶有測量值的示例檔案和一個預期檔案,然後測試執行器將針對該組檔案處理實現,並確保結果正確。另一件事也非常重要,我必須在那臺機器上執行所有這些程式。有很多事情與此相關,例如確保機器配置正確。對所有程式我都會執行五次,丟棄最快和最慢的結果。
簿記(Bookkeeping)
然我們再談一件事,也是非常重要的,就是簿記。回到我展示的初始程式碼,裡面有一個 Java Streams 實現,我使用這個收集器將值分組到不同的儲存桶中,每個氣象站名稱都是如此。人們意識到,這裡也是可以做大量最佳化的。憑直覺,你會為此使用 HashMap。你將使用氣象站名稱作為該 HashMap 中的鍵。Java HashMap 是一種通用結構,適用於一系列用例。然後,如果我們想為一個特定用例獲得最佳效能,那麼我們最好自己實現一個定製的資料結構。
這就是我們在這裡看到的,它比較簡單。這裡我們想跟蹤每個氣象站名稱的測量值。它就像一張地圖,由一個數組支援。
現在的想法是,我們獲取車站名稱的雜湊鍵,並將其用作該陣列中的索引,在陣列中的特定位置,我們將管理特定車站名稱的聚合器物件。我們獲取雜湊碼,並希望不會溢位。這就是為什麼我們將其與陣列大小的邏輯結尾一起獲取。我們始終在陣列中對其進行良好的索引。然後我們需要檢查,在陣列中的特定位置是否已經存在某些內容?如果沒有,則意味著我們手中有特定車站的第一個例項,因此是漢堡市的第一個值或慕尼黑的第一個值。我們只需在那裡建立這個聚合器物件並將其儲存在陣列中的特定偏移量處。當然另一種情況是,我們轉到陣列中的特定索引,並且那裡很可能已經存在某些內容。如果你有同一車站的另一個值,則那裡已經存在某些內容。
問題是我們還不知道,這實際上是我們手中的特定鍵的聚合器物件,還是其他東西?因為多個站名可能具有相同的鍵。這意味著,在這種情況下,如果某個東西已經存在於這個特定的陣列槽中,則需要回退並對比實際名稱。只有當傳入的名稱也是該槽中聚合物件的名稱時,我們才能將值新增到該名稱中。這就是為什麼它被稱為線性探測。否則,我們將繼續在該陣列中迭代,直到我們找到一個空閒的槽,然後我們可以在那裡安裝它,或者我們找到了我們手中的鍵的槽。對於這個案例,它的效能實際上比我們僅使用 Java HashMap 所能獲得的效能要好得多。
當然,這在很大程度上取決於我們用來查詢該索引的特定雜湊函式。這可以追溯到人們真正針對特定資料集進行了大量最佳化的工作上,他們使用了對特定資料集無衝突的雜湊函式。這有點違揹我的想法,因為問題在於這個檔案,正如我提到的,它的大小為 13 GB,而我沒有很好的方法將 13 GB 分發給大家,所以他們必須自己生成它。我有資料生成器,每個人都可以使用這個生成器為自己建立檔案,然後將其用於自己的測試目的。問題是在這個資料生成器中,我有一個特定的金鑰集。我有大約 400 個不同的車站名稱,這只是一個例子,但人們非常字面地理解了它,然後他們針對這 400 個車站名稱進行了大量最佳化。他們對這 400 個名稱使用了不會發生任何衝突的雜湊函式。人們會利用他們能利用的一切。
所有這些的問題在於它也給我帶來了很多工作,因為你無法真正證明沒有雜湊衝突。實際上,每當有人提交他們的實現時,我都必須去檢查他們是否真的處理過這種情況,即兩個站會建立相同的金鑰,他們是否相應地處理了這些衝突?因為如果你不這樣做,就會回到緩慢的時代,你會非常快,但你會不正確,因為你沒有正確處理所有可能的名稱。這是我為自己設定的一個小陷阱,這意味著我總是必須檢查這一點,並在拉取請求模板中詢問人們,如果你有一個自定義對映實現,你在哪裡處理衝突?
GraalVM:JIT 和 AOT 編譯器
上面提到了三件大事,並行化,然後是使用 SIMD 和 SWAR 處理所有這些解析,以及用於簿記的自定義雜湊對映。然後還有更具體的技巧,我會提到其中的幾個,想給你一些現成的靈感。
現有的一個工具是 Epsilon 垃圾收集器,這是一個非常有趣的垃圾收集器,因為它不收集任何垃圾。這是一個無操作實現。如果你有常規的 Java 應用程式,它不會是一個好主意。因為你會不斷分配物件,如果你不執行任何 GC,就將在某個時候耗盡堆空間。在這個挑戰中人們意識到,我們實際上可以以一種在處理迴圈中不進行任何分配的方式來實現這一點。我們最初在載入程式時會做一些分配,但稍後不會再建立任何物件。我們只有可以重用的陣列,就像可變結構一樣,我們可以更新它們。
然後我們就不需要任何垃圾收集了,也不需要任何 CPU 週期花在垃圾收集上,這意味著我們可以更快一點。我認為這是一件有趣的事情。另一件事,你可以看到這裡人們使用了很多 GraalVM。GraalVM 實際上有兩個作用。它是一個提前編譯器,因此它會獲取你的 Java 程式並從中發出原生二進位制檔案。這有兩個優點。首先它佔用更少的記憶體。其次它啟動速度非常快,因為它不必進行類載入和編譯等所有操作,這一切都發生在構建時。如果你有這個原生二進位制檔案,它啟動速度很快。
一開始我認為在啟動時節省幾百毫秒對處理 13 GB 的檔案不會有什麼影響,但實際上它確實有影響。AOT 編譯器和大多數最快的實現實際上都使用了帶有 GraalVM 的 AOT 編譯器。也可以使用它來替代 JVM 中的即時編譯器。你可以將其用作 C2 編譯器的替代品。我並不是說你應該總是這樣做。這有點取決於你的特定工作量和你做的事情。而這個問題非常適合這樣做。人們只需使用 GraalVM 作為 JVM 中的 JIT 編譯器,就可以獲得大約 5% 的良好改進。我推薦你嘗試一下,因為它基本上是免費的。你只需要確保你使用的 JVM 或 Java 發行版有 GraalVM 作為 C2 編譯器的替代品。
還有其他一些東西,比如不安全。我發現上圖右邊的構造很有趣,如果你看一下,這是我們的內部處理迴圈。我們有一個 scanner 物件。我們嘗試獲取下一個值,嘗試處理它們,等等。這裡看到的是,我們在以順序方式編寫的程式中有三次相同的迴圈。你會說這三個迴圈是一個接一個地執行。實際發生的情況是,由於 CPU 有多個執行單元,編譯器會發現這實際上可以並行化,因為這些迴圈之間沒有資料依賴關係。我們可以採用這些迴圈並同時執行它們。我發現這很有趣。為什麼是三次?因為是經驗決定的。想出這個主意的 Thomas 網友嘗試了兩次、四次,結果發現三次迴圈是該機器上最快的。在其他機器上可能會有所不同。
結   果
那麼你一定很好奇,我們最後能跑多快?
這是我一開始的 8 個 CPU 核心的排行榜。當我轉移到自己的本地機器時,試圖保持相同的速度。利用了這 8 個核心後,我們縮短到了 1.5 秒。我沒想到 Java 能跑得這麼快,在 1.5 秒內就能處理 13 GB 的輸入。我覺得這相當令人印象深刻。
情況會變得更好,因為我有了一臺強大的伺服器,有 32 個核心和 64 個執行緒,還有超執行緒。當然,我想看看我們能跑多快?然後我們縮短到了 300 毫秒。
對我來說這簡直令人難以置信,超級令人印象深刻。此外,正如我所提到的,人們在其他語言、其他平臺上也做到了這一點,而 Java 確實處於領先地位,所以在其他平臺上你不會有太大的優勢。
還有另一項評估,因為我提到我有一個包含 400 多個站名的資料生成器,人們透過選擇特定的雜湊函式等對其進行了大量最佳化。有些人意識到這實際上不是我的本意。我想看看這一般來說能有多快?於是我們有了另一個排行榜,其中實際上有 10k 個不同的站名。
正如你看到的,它實際上有點慢,因為你真的無法針對該資料集進行那麼多最佳化。此外,這裡排名靠前的人也不一樣了。
漫長的旅程
人們為此工作了很長時間,挑戰持續了整個 1 月。當人們想出某種技巧時,其他人也會很快將其採納到自己的實現中。這是一段漫長的旅程。
上面是 Roy Van Rijn 的實現,他儲存了非常好的日誌,你可以看到他隨著時間的推移是如何進步的。如果你往下看,你會看到他開始有點掙扎,因為他做了一些改變,實際上它們比以前慢了。問題是他跑在了他的 Arm MacBook 上,這顯然與我執行它的機器有不同的 CPU 和不同的特性。他看到了一些本地改進,但實際上在評估機器上速度更快。你可以在底部看到,他嘗試購買一臺 Intel MacBook,以便有更好的機會在本地執行某些操作,並且該操作在那臺機器上的效能也會更好。我發現 Java 中也發生了這種情況,這真的很令人驚訝。這個層面如此深入,特定的 CPU 甚至它具體是哪一代都會在這裡產生影響。
你應該這樣做嗎?
你應該做這些最佳化工作嗎?這要視情況而定。如果你在開發企業應用程式,我知道你大多數時候都在處理資料庫 I/O。達到那個級別並試圖避免業務程式碼中的 CPU 週期可能會很浪費時間。而如果你要應對這樣的挑戰,那麼這可能是一件有趣的事情。我建議的是,例如某個實現比我的基線快一個數量級。它仍然非常易讀,你在這方面沒有任何陷阱。這種最佳化就很不錯,你應該非常清楚你是否想這樣做。
或者如果你想加入 GraalVM 團隊,也可以嘗試一下極限最佳化。我前幾天才知道,在比賽中一位名叫 Mary Kitty 的選手被 Oracle 的 GraalVM 編譯器團隊聘用了。
總   結
這場挑戰不僅影響了 Java 社群,還影響了其他生態系統。在 Snowflake 中,他們發起了一項“一萬億行挑戰”。
GitHub 儲存庫中有這個挑戰的展示和說明。你可以去那裡看看 Rust 和 OCaml 中的所有實現,以及我從未聽說過的所有好東西,看看他們以非常友好、有競爭力的方式做了些什麼事情。
上面是一些統計資料。
從經驗教訓來看,如果我想再來一次,我必須在規則方面真正規範化,實現更多自動化,並與社群合作。Java 語言慢嗎?我認為我們已經揭穿了這一謊言。明年我會再做一次嗎?我們拭目以待。
原文連結:
https://www.infoq.com/presentations/1brc/#idp_register
宣告:本文由 InfoQ 翻譯,未經許可禁止轉載。
今日好文推薦
DeepSeek 開源周過後,國產晶片廠在焦慮中狂歡
騰訊元寶連夜修改使用者協議!“霸王”條款衝上熱榜,你的內容到底誰說了算?
“抄襲”程式碼,到底是 CTO 的鍋還是創始人的鍋?!這事兒已經撕3天了
分散式系統程式設計已停滯?!

相關文章