後端工坊 2026 年 10 月 13 日

2026-10-13 — free-threaded Python 以惰性檢視解析 JSON 達 65.7 GB/s

primary=https://lemire.me/blog/2026/10/12/parsing-json-at-tens-of-gigabytes-per-second-in-python/ primary=https://pypi.org/project/fastsimdjson/

free-threaded Python 以惰性檢視解析 JSON 達 65.7 GB/s

Daniel Lemire 部落格 · 2026-10-12 · fastsimdjson 0.6.0

Python 解析 JSON 的瓶頸,從「解析器不夠快」變成「建立 Python 物件太貴」:fastsimdjson 的惰性檢視(lazy view)只為被存取的欄位建物件,在 free-threaded Python 3.14 上用 64 執行緒,達到 65.7 GB/s。

這是 simdjson 作者 Daniel Lemire 在 2026-10-12 公布的基準測試,使用他維護的 Python 綁定 fastsimdjson 0.6.0。數字全部來自作者自己的測試環境,並非第三方重現。

原本的問題

CPython 長期受全域直譯器鎖(GIL)限制,同一時間只有一個執行緒能跑 Python 程式碼,多核心對 JSON 解析幫助有限。Python 3.14 提供 free-threaded 版本(python3.14t),移除了 GIL,多個執行緒終於能同時跑 Python 程式碼。

但只拿掉鎖還不夠。標準庫 json、orjson,甚至 fastsimdjson 非惰性模式,都會把整份文件轉成 Python 物件,而多數物件馬上就被丟棄。作者指出,這些方法的大部分時間花在建立物件上,而不是解析。

測試設計與惰性檢視

題目是統計 GH Archive 的事件:取 2024-03-13 的八小時資料,解壓後 7.6 GB,共 2.2 million 筆事件,每筆約 3.4 kB。程式要數出各事件類型,並對每個新開的 pull request 查出目標儲存庫的語言。結果是 65,269 個 pull request,其中 TypeScript 最多(11,565)。

fastsimdjson 的 map_many 對每筆事件呼叫使用者函式,並可指定執行緒數。傳入 lazy=True 時,函式拿到的是惰性檢視:事件已被解析,但只有被存取的值才會建立 Python 物件。這個做法最早由 Tyler Kennedy 在 pysimdjson 中發展。

fastsimdjson.map_many(one_event, data,
    format="lines", threads=64, lazy=True)

每個執行緒各自持有一組計數器,執行緒之間不共享,因此不必互相等待;全部完成後主執行緒再合併,作者說合併成本相對於解析可忽略。

實際效果

測試機為兩顆 Intel Xeon Gold 6548N(64 核心),Python 3.14.6。比較對象含 orjson 3.13.0 與 msgspec 0.22.0,其中 msgspec 預先宣告只需要的欄位,作者說這是讓它占便宜。單核心結果如下。

方法(單核心)GB/s
json(標準庫)0.18
orjson0.42
fastsimdjson map_many0.63
msgspec(型別結構)1.29
fastsimdjson map_many(lazy)2.13

標準庫跑完八小時資料要 42 秒,惰性模式只要 3.6 秒。換成 64 執行緒後差距更大。

方法(64 執行緒)GB/s
json(標準庫)3.17
msgspec(型別結構)24.6
fastsimdjson map_many29.8
fastsimdjson map_many(lazy)65.7

7.6 GB 約 0.12 秒處理完,是單核心的 31 倍,也快過 64 執行緒的 msgspec(24.6 GB/s)。作者的標準庫與單執行緒解析器數字是單次執行,其餘取七次最佳值,且資料在計時前已在記憶體中解壓,所以這不含磁碟與解壓時間。

影響範圍

處理大量 NDJSON 日誌、事件串流的批次作業最直接受益,例如只需要幾個欄位的指標彙整。收益主要來自「只讀少數欄位」:若每個欄位都要取用,惰性檢視的優勢會縮小,作者的非惰性數字(0.63 GB/s 單核心)才是參考。

使用上有幾個前提要檢查。第一,要跑 python3.14t,一般帶 GIL 的版本無法取得執行緒擴展的效果。第二,fastsimdjson 的 PyPI 頁面標示提供 Python 3.10 至 3.14 的 wheel,含 free-threaded 3.14,並可作為 json.loads 的替代品,但惰性 parse 與 map_many 是它自己的 API,需要改寫程式。第三,若已用 msgspec 且 schema 固定,單核心 1.29 GB/s 已不差,是否換用要看欄位比例。

基準程式碼公開在作者的儲存庫,可用自己的資料重跑。

原始來源:Lemire 部落格、fastsimdjson on PyPI、基準程式碼


End of article
0
Would love your thoughts, please comment.x
()
x