ARM64_Lab

json.loads が 105.3ms、正規表現 compile 差が小さかった話

この記事の見出し
  1. JSON とログ文字列を標準ライブラリだけで触る
  2. dumps と loads、regex と split
  3. compile すれば勝ち、ではなかった
  4. split はなぜ負けたのか
  5. 自分の使い分け

50,000件を入れた JSON は、json.dumps が82.73ms、json.loads が105.3ms だった。

読むほうが書くより1.3倍重い、というところまではまだ分かるとして、予想と違ったのは正規表現のほうだった(この時点では compile の効果をかなり大きく見ていた)。re.compile してから使えばはっきり速くなると思っていたのに、手元では7.51ms 対 8.59ms で、差は1.1倍に留まった。Python が文字列パターンを内部でキャッシュしているので、毎回 re.findall に同じパターンを渡しても、思っていたより損は小さい。

もっと意外だったのは、単純な str.split が15.37ms まで伸びたこと(軽いはず、という思い込みが先にあった)。split のほうが軽いはず、という先入観があったので、同じ情報を取るだけなら正規表現のほうが速いという結果は、少し気持ち悪かった(ここは測ってみてよかった箇所)。

計測日は2026年6月18日。走らせたのは20時18分ごろで、記録に残った日付は2026-06-18だった。環境は Surface Pro 11th Edition、Snapdragon X 12-core X1E80100、Windows 11 Pro 10.0.26200 ARM64、Python 3.12.10 ARM64。電源は AC 接続で、電源プランはバランスのままにした。

JSON とログ文字列を標準ライブラリだけで触る

標準ライブラリだけで測ったので、外部ライブラリの最適化は混ざっていない(比較範囲を狭く固定した)。JSON 側は50,000件のレコードを一度 json.dumps し、その文字列を json.loads で戻す。生成された JSON は5,538,890 bytes、10進の MB で見ると5.54MB になる。

payload = json.dumps(records, separators=(",", ":"))
obj = json.loads(payload)

ログ側は20,000行のテキストを作り、同じ正規表現で ID、level、path のような値を抜く。比較したのは、先に re.compile したケース、文字列パターンをそのまま re.findall に渡すケース、そして splitlinessplit で分解するケースの3つ。

pattern = re.compile(r"id=(\d+) level=(\w+) path=([^ ]+)")
matches = pattern.findall(text)
matches = re.findall(r"id=(\d+) level=(\w+) path=([^ ]+)", text)

各ケースは5回ずつ測り、中央値を見るようにして、1回だけの最速値は使わない(外れ値で気分よくならないための保険でもある)。ARM64 Windows の体感に近づけたいので、あえて高パフォーマンス設定へ寄せず、普段の Surface Pro のまま走らせている。

dumps と loads、regex と split

全反復値をそのまま置くことにしたのは、中央値だけを抜き出すと表がきれいに見えすぎて、実際にはどの回が暴れていたのかを後で思い出せなくなるからだ(この手の小さい測定では、そこがけっこう大事になる)。JSON は records が50,000件、json_bytes が5,538,890 bytes。テキストは text_lines が20,000行だった。

ケース 対象 runs_ms(5回) median_ms
json.dumps 50,000件 / 5,538,890 bytes 105.18 / 76.54 / 82.34 / 82.73 / 89.65 82.73ms
json.loads 50,000件 / 5,538,890 bytes 105.3 / 112.35 / 108.71 / 98.5 / 101.31 105.3ms
regex_compiled 20,000行 7.51 / 17.85 / 7.11 / 7.04 / 7.65 7.51ms
regex_uncompiled 20,000行 10.09 / 7.34 / 8.59 / 7.15 / 10.54 8.59ms
str_split 20,000行 21.84 / 11.0 / 15.44 / 15.37 / 9.49 15.37ms

json.dumps は5.54MB を82.73ms で処理したので、約67.0MB/s。件数で見ると約604,000件/s だった。json.loads は同じ5.54MB を105.3ms で読むため、約52.6MB/s、約475,000件/s まで下がった。

差は大きすぎないが向きははっきりしていて、JSON は書くより読むほうが重く、ファイルから読み込んでパースする処理を大量に積むと、保存時より復元時のほうで待つ可能性がある(設定ファイルが増えるツールでは、後から効いてくる)。これは地味だが、起動時の設定読み込みでは効いてくる話。

正規表現は、compile 済みが7.51ms、未 compile が8.59ms。20,000行なので、compile 済みは約2,663,000行/s、未 compile は約2,328,000行/s になる。速くはなるが、1.1倍という差は体感より小さい。

str.split は15.37ms で、約1,301,000行/s。名前だけ見るとこちらが軽そうなのに、今回の作りでは正規表現の半分くらいの速度だった。splitlines で20,000行ぶんのリストを作り、さらに各行を split するため、素朴に書いた分だけオブジェクト作成のコストが乗ったのだと思う。

compile すれば勝ち、ではなかった

最初に表を見たとき、regex_compiled の17.85ms が目立っていて、compile 済みのほうがむしろ不安定に見えた。ここだけを拾うと、測定が失敗したようにも感じる。けれど5回の中央値は7.51msで、ほかの4回は7ms台。1回の外れ値に引っ張られると、判断を間違えやすい。

もうひとつの勘違いは、re.compile の効果を大きく見積もっていたこと。Python の re は最近使ったパターンをキャッシュする。だから同じ文字列パターンを何度も渡すだけなら、毎回フルにコンパイルしているわけではない。知識としては知っていたつもりでも、7.51ms と8.59msが隣に並ぶと印象が変わる(速度差だけを目的にするなら、思ったほど劇的ではなかった)。

これは「compile しなくてよい」という結論ではない。複雑なパターンを何種類も使う、ループの外へ意図を出したい、例外を早めに出したい。そういう理由なら compile する意味は残る。ただ、速度だけを理由に毎回書き換えるほどではなかった、という整理だ。

split はなぜ負けたのか

str.split は組み込みメソッドなので、正規表現より速いはずだと思っていた。今回の測り方では、その直感が外れている。では何を比べていたのだろうか。

実際に比べているのは、単発の split ではない。20,000行の文字列を splitlines で行リストにしてから、各行をさらに分ける。必要な列を取り出すために、複数の一時文字列やリストが生まれる。正規表現は1回の走査で必要なグループだけを返せるので、今回のログ形式ではそちらが合っていた。

この差は15.37ms と7.51msなので、約2.0倍ある。巨大なログ処理なら気にする価値があるが、短い設定ファイルを読む程度なら誤差かもしれない。個人的には、読みやすいほうを先に書き、20,000行を超えるような処理で初めて測る、くらいの扱いにする。

自分の使い分け

JSON は、保存より復元を疑う方針にした。今回の5.54MBなら100ms前後で済むが、件数が増えれば差は積み上がる。特に起動時に複数ファイルを json.loads するツールでは、体感の引っかかりになりやすい。

正規表現は、速度目的だけで re.compile を絶対視しない。繰り返し使うパターンなら compile して変数名を付けるが、1か所で完結する短い処理なら文字列パターンのままでもよい。手元の Python 3.12.10 では、その差は1.1倍だった。

範囲も明確にしておく必要があり、今回のログは標準ライブラリの jsonre を触っただけの記録で、別ライブラリや別マシンの話まで同じ結論に混ぜるつもりはない(ここを広げると、また測定条件がずれる)。今回は標準ライブラリだけの比較で、orjson のような高速ライブラリとは比べていないし、実は別環境ならその比較もやりたいが、このログではそこまで広げずに止める(広げ始めると別記事になりそうだ)。2026-06-18 の時点で、ARM64 Windows 上の標準の jsonre がどのくらい動くかを見た記録として残しておく。

a
arm64lab — 個人運営

Surface Pro 11th Edition(Snapdragon X Elite)を2025年5月から常用機にしている個人の記録です。ARM64 版 Windows で詰まったところと、その場で測った値をそのまま書き残しています。特定の企業・団体とは関係がなく、いかなる組織を代表する見解でもありません。