這個系列的標題是一道算式,今天來驗算一下。
Spark 這個 1,價值不是執行引擎,是 Catalyst。十年份的優化規則、統計、AQE,加上整個生態的 API 與部署慣性,沒有人想重寫它,也不需要重寫。
DataFusion 這個 1,價值是從第一天就把自己設計成可嵌入、模組化的引擎,它不預設誰是宿主,所以誰都能當宿主。
Comet 這個 1 最特別,它做的是把 Spark 的 physical plan 翻譯成 DataFusion 的 ExecutionPlan,然後負責兩邊的記憶體怎麼交接。它不是第三個加數,它是那兩個加號。
二十九天的內容可以收成同一句話:資料在哪裡,誰負責搬它。
硬體那層說,資料要待在 cache 裡、待在暫存器裡,所以有了欄式佈局、SIMD,以及 8192 這個被整棵 plan tree 折衷出來的批次大小。儲存那層說明了,最便宜的搬動是不搬,所以有了三層剪枝漏斗與晚物化。引擎那層說明,搬不掉的就邊搬邊算,所以有了 pull-based streaming、spill 與提前終止。表格式那層說名,搬過的東西要能被找到、也要能被回頭找到,所以有了 manifest 兩層與 snapshot 共用。跨界那層說名,搬到別人手上時要講清楚這塊記憶體是誰的,所以有了 Arrow 記憶體佈局與 release callback。
大於號就是從這裡來的,Spark 自己搬不動的那些位元組,交給一個能控制記憶體佈局、能用滿 SIMD、不用跟 GC 商量的執行層去搬,單看執行這一段,收益是實打實的。
protobuf 那道 IR 要序列化(D24)。Arrow FFI 那條邊界要傳 struct、要解指標鏈、遇到非零 offset 還會退回一次真正的複製(D25)。算完之後結果要轉回列式塞回 Spark,C2R 這筆是結構性的,只要終點還在 Spark 手上就免不了(D26)。表達式沒有原生實作時,計畫上會被撕出一道縫(D27)。
D0 引的那個量測結果現在有解釋了:EDBT 量到 vanilla Spark 在某些查詢仍然贏過所有 native 引擎。那不是誤差,那是加號比收益貴的時候。中間結果一大、跨界次數一多、或者向量化被迫每個運算子物化一次,帳就翻過去了。
所以誠實的寫法是:1+1+1 > 3 只在加號夠便宜的時候成立,否則它是小於。 而過去這幾個版本 Comet 在做的事,幾乎全是在壓低加號的價錢,從 0.14 的原生 Rust C2R、0.17 的 C Stream Interface 與 validity buffer 快取,到 1.0 把 fallback 粒度細化到表達式。沒有一項是讓算得更快,全部是讓接得更便宜。
Velox 選擇當被嵌入的函式庫,Photon 選擇跟自家 runtime 綁死換取整條路徑的掌控。三種路線的差別,本質上就是把加號畫在哪裡、以及願意為它付多少。
標題後半叫效能煉金術,但這三十天沒有任何一步是魔法。
煉金術真正的遺產從來不是點石成金,是它在失敗的過程裡長出了化學。這條技術棧也一樣:沒有任何一層是憑空生出效能,每一層都只是把成本從一個地方搬到另一個地方,然後賭搬過去的那邊比較便宜。看懂搬到哪、賭的是什麼,比記住哪個版本快多少有用得多。
具體的版本細節(Comet 1.0.0、DataFusion 54.1、Arrow 58.4)一定會過期,有些設定名稱可能明年就改掉了。但那句資料在哪裡,誰負責搬它不會過期,因為它是硬體決定的,不是軟體決定的。這道算式往後是大於還是小於,也會隨兩邊各自演進而變,但驗算的方法不會變:先看三個 1 各自值多少,再看加號收多少。
三十天到此結束~~~~~謝謝一路看下來的你 🙏