11月14日、Daniel Lemireが「Reading JSON at 277 GB/s in Python」と題した記事を公開した。Pythonで大量のNDJSONを処理する際、標準のjsonモジュールでは0.28 GB/sしか出ないスループットを、free-threaded Python(GIL廃止ビルド)とSIMDベースのJSONパーサーを組み合わせることで277 GB/sにまで引き上げた実験の報告だ。
LemireはJSONパーサーsimdjsonの作者として知られる研究者であり、今回の実験はそのPythonバインディングを追求した取り組みの最新形だ。単なるベンチマーク記録の更新にとどまらず、「GILがなくなったPythonでスレッド並列がどこまで効くか」「プロセス並列との差はどこで生まれるか」という実践的な問いへの回答になっている点が面白い。
題材:GitHubイベントログの集計
GH ArchiveはGitHubの公開イベント(スター付与・プッシュ・フォークなど)を1時間ごとにNDJSON(Newline Delimited JSON:1行1レコードのJSON形式)で公開している。1時間あたり約1GBで、8時間分で7.6GB・220万イベントにのぼる。「直近8時間でスターを最も多く受けたリポジトリは?」といった集計を行うだけでも、この規模のデータを走査する必要がある。
標準ライブラリのjsonモジュールを使った素朴な実装はこうなる:
for line in open("events.ndjson", "rb"):
event = json.loads(line)
if event["type"] == "WatchEvent":
stars[event["repo"]["name"]] += 1
これは0.28 GB/s、7.6GBで約27秒かかる。orjsonで約2倍、必要なフィールドだけをデコードするmsgspecで約8倍まで伸びるが、Lemireが今回目指したのはその先だ。
fastsimdjsonとfree-threaded Pythonの組み合わせ
PythonはGIL(Global Interpreter Lock)の制約により、従来はスレッドを使ってもCPUバウンドな処理を並列化できなかった。GIL廃止はPEP 703として提案され、Python 3.13で実験的に導入、2025年10月にリリースされたPython 3.14で正式化された。python3.14tとして配布されるfree-threaded buildにより、複数スレッドが真の意味で並列にPythonコードを実行できる。
fastsimdjsonはこのfree-threaded環境向けに設計されたライブラリだ。SIMDを活用した高速JSONパーサーsimdjsonをバックエンドに、複数スレッドで並列処理を行う。設計上のポイントは「レコード全体をPythonオブジェクトに変換しない」点で、指定したJSONパス(例:/type、/repo/name)の値だけを取り出して関数に渡す。
import collections
import fastsimdjson
def starred(event):
kind, repo = event
if kind == "WatchEvent":
return repo
names = fastsimdjson.map_many(
starred, data, threads=192,
fields=["/type", "/repo/name"], drop_none=True)
stars = collections.Counter(names)
dataはファイルをメモリマップしたバイト列。drop_none=TrueでNoneを返したレコード(WatchEvent以外)を除外する。
実測結果:192コアで55ミリ秒
テスト環境はAWSのc8a.metal-48xl(AMD EPYC 9R45 × 2ソケット、192コア)、Python 3.14.10。GH Archiveの7.6GBデータに対するスター集計(カウント集計)の結果は以下の通りだ。
| 手法 | スループット |
|---|---|
json(標準ライブラリ) |
0.28 GB/s |
orjson |
約0.56 GB/s |
msgspec |
約2.2 GB/s |
fastsimdjson(1スレッド) |
8 GB/s |
msgspec × 96プロセス(プロセスプール) |
116 GB/s |
fastsimdjson × 192スレッド |
138 GB/s |
7.6GBのスター集計が55ミリ秒で完了する。1コアではmsgspecの3.5倍だった差が、フルスケールでは約20%まで縮まる点は興味深い。スケールアップに伴うオーバーヘッドや、集計ロジック(Counterへの書き込み)がボトルネックになっていると考えられる。
別途、既存ベンチマークPisonが用意する6種類のJSONファイルで「値の抽出」処理を試した結果も記事内で示されている。こちらはカウント集計と異なり書き込み競合が少ないためスケールしやすく、Twitterデータでは277 GB/sを記録。6ファイル中4ファイルが100 GB/sを超えた。一方、残り2ファイルはそれぞれ約20 GB/sおよびmsgspecと同等にとどまっており、スループットがファイル構造によって大きくばらつく点は実運用で注意が必要だ。
プロセス並列との比較
multiprocessingによるプロセスプールでも116 GB/sという高いスループットが出ているが、プロセス並列はデータのコピーコストが問題になる場面がある。「少量の集計結果だけを返す」カウントのようなユースケースでは効率的だが、大量の値そのものを呼び出し元に戻す処理では、プロセス間コピーがボトルネックになりやすい。スレッドベースのfastsimdjsonはメモリを共有するためこの制約を受けない。
free-threaded PythonはPython 3.14時点でも開発途上の部分があり、すべての拡張ライブラリがfree-threaded環境に対応しているわけではない。しかし「GILを外しただけでここまでスケールする」という実証は、今後のPythonエコシステムの方向性を示す事例として参考になる。
fastsimdjsonのソースコードはGitHubで公開されている。
詳細はReading JSON at 277 GB/s in Pythonを参照していただきたい。