SQLite parse_number():重寫解析器後,C 擊敗 Rust
Simon Willison 的 SQLite parse_number() 實驗起初顯示 Rust 領先 C,直至針對性重寫解析器後,效能排名出現逆轉。這項於 2026 年 11 月 8 日發表的調查,比較了用於清理格式化數字的擴充功能——即加入資料庫函數的已編譯附加元件。據報,最終的 C 實作在所有受測資料集均居首,同時保留較小的已編譯檔案。
這項工作聽來不算複雜:把含有貨幣符號、逗號或百分號的文字,轉換成適合數值排序與加總的值。然而,這項比較對不同工程問題給出了不同答案。Rust 提供了較快的首版小數解析器;經優化的 C 最終提供了測得最快的實作。
對於評估 AI 生成程式碼的開發者而言,壓力落在審核流程上。一個看似合理的實作和一種熟悉的語言,幾乎無法證明執行時間實際花在哪裏。Willison 的調查在量測找出某個特定轉換常式、並促成更聚焦的實驗後,才變得有價值。
SQLite parse_number() 在第二輪後改變結果
隨着實作改變,報告中的勝方亦告改變,令解析器設計成為比較的核心。
Willison 起初要求其帳戶中標示為 Opus 5.5 的 Claude,以 C 和 Rust 探索這個函數。他要求的比較涵蓋執行速度、程式碼複雜度及產生的函式庫大小。他在其 SQLite parsing experiment 中描述了這項調查。
首輪結果把檔案大小與速度區分開來。Willison 報告指出,C 擴充功能約為 14 KB,而使用 rusqlite 的版本為 321 KB、使用 sqlite-loadable-rs 的版本為 298 KB。這些 Rust 函式庫把 Rust 程式碼連接至 SQLite。儘管檔案較小,首版 C 實作在混合及小數輸入上落後。
解釋集中於 strtod(),這是一個把文字轉換成浮點數的 C 函式庫函數;浮點數是一種近似的數值表示。Willison 將 Rust 初版的小數優勢歸因於其較快的轉換常式。其後,他要求 Claude 產生一個可媲美或超越 Rust 實作的 C 解析器。
修訂後的 C 版本在報告比較中領先。在混合資料上,每列的四捨五入後淨成本由首版 C 的 67 納秒降至修訂版 C 的 19 納秒。Rust 版本則錄得 46 及 41 納秒。只含小數的輸入在 C 內部顯示出更大的變化,由每列 106 納秒降至 18 納秒。
這個過程正是故事的重要之處。若在首次比較後停止,量測的小數工作負載將支持選擇 Rust。繼續作出針對性修訂後,則產生了不同的候選方案,而所要求的資料庫函數並未改變。
這項練習亦把產生替代方案與評估它們區分開來。Claude 提供了實作,但它們的相對價值是透過周邊量測才顯現。首個答案提供的是起點,而非已定案的工程決策。
沒有理由把這個結果視為程式語言的普遍排名。這些只是小型函數的特定實作。它們的差異包括轉換常式、暫存儲存空間及整合程式碼,這些因素全都隱藏在寬泛的 C 對 Rust 標題之下。
實際改變更為具體:開發者現在有一個已發表的例子,顯示較慢的首版 C 草稿如何成為測得最快的候選方案。這使案例適合用於檢視實作選擇,同時不支持更廣泛的語言主張。
基準測試量度 SQLite 內部的重複解析
這項工作負載令重複轉換成為核心,因此解讀結果時須附帶這個背景。
已發表的 benchmark harness 建立了一個包含 200 萬列的記憶體內資料庫。它評估一項會解析每個值並將結果加總的查詢。Python 啟動查詢,而 SQLite 執行重複的資料庫工作。
這種安排很重要,因為它避免讓 Python 迴圈包圍每次個別轉換。因此,實驗更直接地針對資料庫函數呼叫與解析路徑的組合。它主要不是量度 Python 應用程式反覆跨入原生程式碼的表現。
測試工具會在 15 次執行中交替測試候選方案。它記錄最短及中位數的經過時間,然後根據最短時間計算所宣傳的淨數值。在結果換算為每列納秒前,會先扣除一項將字串長度加總的基線掃描。
這項扣除有助隔離增量工作,但該數字仍是一項比較估算。它不是匯入、儀表板重新整理,或涉及其他操作的查詢之完整耗時。把它解讀為面向使用者的回應時間,會把量測延伸至其設計範圍以外。
資料集亦塑造結果。獨立執行涵蓋整數文字、小數文字及混合格式化數值。混合產生器包括附有貨幣標記的數值、百分比、一般數字字串及無效項目。這些變化讓人可區分小數轉換問題與一般解析問題。
設想一個反覆排序已匯入財務報告的應用程式。若每次查詢均進行解析,轉換工作便會成為每項報告請求的一部分。若應用程式改為在匯入時標準化數值,同一解析器便會影響不同階段,並面對不同的效能要求。
兩種架構均不會自動由這些量測推導出來。實驗提供了解析元件的證據,而應用程式決定該元件的執行頻率。當一個較快的獨立函數在實際工作負載中佔有重要比例時,其影響最為顯著。
基準測試的純 SQL 替代方案使用巢狀 replace() 呼叫,即重複的字串替換,然後進行數值轉換。它為開發者提供一個熟悉的比較點。然而,替換字元與執行完整輸入政策是不同工作,因此是否行為等價需另行檢視。
因此,壓力落在使用簡短基準測試摘要來支持實作選擇的開發者身上。在採用所報告的排名前,他們需要確認解析在其應用程式中何處發生,亦需要判斷自己的輸入是否與產生的資料集相似。
對 SQLite parse_number() 而言,這個測試工具令這些問題可供檢視。讀者可檢查計時查詢及資料產生器,而非只依賴一個勝出數字。
C 透過合併驗證與轉換取得速度優勢
經優化的 C 實作減少中間工作,同時為超出捷徑限制的輸入保留備援處理。
optimized decimal parser 在累積數值資訊的同時檢查字元。在掃描期間,它追蹤有效數字並計算小數點後的位數。這讓合適的輸入毋須重建為清理後的字串。
其捷徑使用 Clinger’s fast path,這是一種適用於中間值符合精確表示要求時的十進制轉換技術。實作以明確數值條件限制這條路徑。超出該等條件的數值會繼續走較慢的轉換路徑。
這項區別至關重要。若一條捷徑只是接受捨入誤差,它回答的將是不同的工程問題。在此,程式碼嘗試加速受限的一組輸入,同時保留預期的數值結果。
實作最多收集 19 個有效數字。其小數捷徑另要求累積值不得大於 2^53,且小數位不得多於 22 位。這些界線決定直接計算何時合資格。
更廣泛的設計解釋了為何修訂改變比較結果。驗證本身已要求檢查輸入字元。在檢查期間累積有用數值資訊,便可避免原本會由獨立轉換階段重複進行的部分工作。
整數處理亦從同一方法受益。當數值符合條件時,函數可直接傳回累積的整數。它不需要先建立一個臨時的清理字串,再把該字串再次轉換。
備援處理仍涉及大量工程工作。它重建已驗證的數字文字,在可行情況下使用本地緩衝區,並在必要時配置額外儲存空間。此處的配置是指為操作要求暫存記憶體,因而引入算術本身以外的處理。
程式碼亦會在啟用捷徑前檢查編譯器的浮點運算求值行為。其防護措施處理了中間計算可能使用不同精度的情況。這個細節顯示,一項小型算術優化可能帶來超出其表面行數的責任。
Rust 仍是有意義的比較,因為其候選方案採用了不同的工作安排。這項調查並未確立一項同樣專門化的 Rust 重寫能否維持相同計時。把經優化的 C 與這些 Rust 版本比較,只能確立這些候選方案的結果。
合理的解讀是,專門化為受測工作負載帶來回報。C 重寫合併了原本分離的工作,並為合資格數值避開通用轉換常式。這項解讀遵循實作中可見的機制。
這亦解釋了為何語言聲譽無法提供完整預測。通用常式可涵蓋廣闊的輸入空間;專門化程式碼則可針對較窄的契約。效能部分取決於所選路徑為實際到達的輸入執行了多少工作。
維護問題隨之而來。誰負責較快的實作,誰便要負責其合資格條件、備援行為及數值假設。只有團隊能在後續變更中保留這些特性時,量測優勢才有用。
擴充功能大小與程式碼擁有權帶來不同取向
較小的已編譯產物與較簡單的維護路徑是獨立準則,兩者均不能互相取代。
專案的 implementation comparison 報告指出,經 strip 處理的 C 函式庫為 14,408 位元組。相應的 Rust 產物,以 rusqlite 製作的為 320,864 位元組,以 sqlite-loadable 製作的為 298,464 位元組。Strip 處理會從已編譯檔案移除某些建置資訊。
同一報告計算出,直接的 C 實作有 75 行非空白、非註解程式碼;經優化的 C 則有 104 行。Rust 使用 36 或 30 行整合程式碼,加上一個共用的 41 行解析器。報告指出 rusqlite 版本有 20 個相依套件,而 sqlite-loadable 有 62 個。
這些量測描述不同形式的複雜度。原始碼長度反映可見的實作規模;相依套件數量反映建置圖的一部分。兩者都不能直接量度日後除錯的難度。
短小函式可依賴分散於多個函式庫的行為。較長而自足的函式,則可把責任集中在應用程式團隊必須審閱的程式碼之中。兩者之間的取捨,取決於團隊希望該責任落在哪裏。
C 實作令多項責任變得可見。其緩衝區處理、整數範圍檢查及十進制捷徑均見於專案自身的原始碼中。審閱者可直接檢查它們,但可見性並不代表不再需要相關專業知識。
Rust 的語言功能與剖析函式庫可減少部分手動處理工作。對已使用 Rust 的團隊而言,這可改善可讀性。不過,精簡的整合層仍須就輸入政策、資料庫行為及錯誤處理接受審閱。
編譯後的大小帶來另一個問題。額外函式庫在資源嚴格受限的發行版本中,與在已包含大量資產的應用程式中,意義並不相同。所報告的差異可以量度;其實際重要性取決於交付環境。
檔案大小同樣不能代表執行時的記憶體消耗。函式庫在磁碟上的位元組數,無法描述處理資料時所作出的每次配置。若把最小檔案視為最低記憶體用量的實作,便需要本比較未有提供的量度結果。
依賴項數目亦應以同樣審慎的態度看待。較多依賴項可能帶來更多建置與更新上的考量,但單靠計數無法確立缺陷率。較少依賴項同樣不能證明由應用程式自行擁有的程式碼較容易維護。
對既有 C 應用程式而言,精簡的 C 擴充套件或可配合既定的編譯及審閱慣例。對既有 Rust 應用程式而言,引入獨立的 C 元件則可能增加額外的維護責任。這些屬於部署判斷,而非基準測試所確立的結論。
因此,SQLite parse_number() 涉及多個彼此獨立的決策維度。所報告的最佳化 C 候選方案在量度到的剖析效能及編譯大小上領先。團隊仍須決定,維護其專門化轉換路徑是否符合自身技能與發行流程。
有用的比較會讓這些維度保持可見。若把它們壓縮為單一贏家,便會掩蓋不同團隊何以能合理地作出不同選擇。
通過測試並不能確定資料契約
正確性不止於各實作是否得出相同數值答案,也包括賦予格式化文字的含義。
比較報告指出,所有版本均通過 40 個明確測試案例及 800 萬次隨機比較。隨機測試通常稱為 fuzzing,會產生輸入以尋找意料之外的行為。這些報告結果提供了證據,但尚未為本文獨立重現。
各實作之間的一致性很有價值,尤其是在算術重寫後。它令測試輸入空間內出現明顯差異的可能性較低。然而,共同一致無法判定所選行為是否符合特定應用程式的業務規則。
百分比正好說明這個分別。移除百分號會保留顯示的數字,而把百分比解讀為比率則會改變其尺度。應用程式必須先決定需要哪一種含義,才可把任何一種行為視為正確。
貨幣標記帶來相關問題。移除符號會令文字較易作數值處理,但不能證明數值使用相同單位。要合併不同貨幣的金額,需要單靠移除字元所無法提供的資訊。
格式慣例亦值得注意。在一種慣例中,逗號可標示數字分組;在另一種慣例中,它可作為小數分隔符。圍繞固定慣例設計的剖析器,需要把這項預期附加到所接收的資料上。
這些是假設性的採用考量,並非 Willison 實作中已確立的缺陷。它們說明剖析器即使完全按設計運作,對陌生資料集仍可能並不合適。正確性始於所選定的契約。
數值精度又增加了一道界線。SQLite 的浮點數文件說明,其 REAL 值採用 binary-64 表示法,因此屬於近似值。無論由哪種語言進行轉換,許多十進制分數均無法以該格式精確表示。
正確捨入的剖析器會選取適當的可表示值,但不會令其後所有算術運算變得精確。需要精確十進制計算的應用程式,必須分開評估其表示法與算術要求。
失敗處理同樣值得重視。當文字無效時,回傳 NULL(SQLite 用以標示缺失或未知值)可讓處理繼續進行。應用程式仍應保留方法,以區分被拒絕的資料與有效數值輸入。
否則,看似整潔的結果可能掩蓋資料品質問題。檢視總數的使用者需要知道,每筆預期記錄是否均有實質貢獻。即使剖析器遵守其契約,這項考量仍屬於周邊應用程式的責任。
基準測試本身亦帶來另一項不確定性。扣除基線後的最短計時,描述的是有利的量度執行結果。它不能確立在生產環境競爭、不同資料集或不同運算環境下的典型效能。
共享測試結果同樣不能推出普遍的效能主張。包含較多異常長輸入的工作負載,可能更常觸發後備路徑。相對於測試中常見格式化數值,這會改變工作量的平衡。
因此,對 SQLite parse_number() 的懷疑式解讀仍具建設性。這些結果支持檢視特定實作並重現其行為,但不會免除定義可接受格式、保留被拒記錄,以及在預定主機應用程式中驗證結果的需要。
留意重現結果、Rust 修訂版及後備行為
下一項有用證據,應在明確保留行為與量度方法的同時,擴大比較範圍。
首個值得留意的訊號是獨立重現。另一個系統上的執行,可測試所報告的排序在硬件及建置環境改變後是否仍然成立。同時報告完整查詢時間及推導出的剖析成本,會令這些證據較易解讀。
如結果相符,將提升對最佳化 C 優勢可延伸至原始設定以外的信心。若排序不同,則會收窄結論,並找出值得調查的另一項因素。只要記錄環境與輸入分布,兩種結果都能改善比較。
重現亦應保留數值結果與計時之間的區分。若快速候選方案改變了可接受的輸入,它便不再回答同一個問題。把正確性結果與效能一併發布,能讓這道界線保持清晰。
第二個訊號,是經修訂的 Rust 實作針對比較中可見的具體工作作出處理。在掃描及轉換策略上更接近的匹配,有助分辨實作結構與語言選擇。相關問題是:可比較的專門化是否會縮小量度到的差距。
若修訂版 Rust 在保留整合優勢的同時縮小差異,初步結果便更有力地支持在選擇語言前先檢視演算法。若最佳化 C 仍保持領先,開發者便會就這個特定元件取得更堅實的證據。
這仍是一項建議中的比較,並非已公布的版本或承諾的結果。沒有理由假設更新版會按固定時間表推出。其價值在於令競爭實作可以更直接地比較。
第三個訊號是關於後備行為的證據。最佳化 C 捷徑可快速處理合資格輸入,但更廣泛的輸入分布可改變較慢路徑的執行頻率。測試這些情況,將更能揭示實作的實際運作範圍。
這項工作應如同檢視速度般仔細檢視正確性。長數字字串、格式錯誤的記錄及不同主機設定,均可暴露常見格式化數值未有揭示的假設。有文件記錄的結果,比另一個孤立的標題計時更能提升信心。
對工程團隊而言,保留周邊證據與保留原始碼同樣重要。建置設定、輸入範例及被拒絕的替代方案,說明了為何選擇某個實作。把它們保存在可搜尋知識庫中,能讓日後審閱者重新檢視該決定。
調查的即時價值已很清楚:量度到的瓶頸帶來具體修訂及改變後的結果。仍未確定的是,該結果可延伸至多廣的範圍。進一步比較應擴大證據,而不應把聚焦實驗變成普遍的語言定論。
採用 SQLite parse_number() 前,先找出應用程式實際面對的決策。保留具代表性的輸入,定義百分比及無效記錄應如何處理,並在實際運行候選方案的主機中比較。完整查詢計時應與任何推導出的剖析數字一併保存,讓有利的元件結果維持與使用者體驗的連結。以審閱後備路徑同等的謹慎程度審閱專門化數值路徑,並保留重現兩者所需的建置細節。下一個有用步驟,是測試此實作的優勢能否符合你的要求。你的應用程式中,哪項限制決定選擇:重複轉換時間、發行版本大小,還是維護剖析器所需的工作?



