大きなJSONファイルの読み書きをする必要があったので、Pythonから使える高速なJSONライブラリのベンチマークを行いました。比較対象のライブラリはChatGPT 4oを使って調査しました。
この記事の結論
- 書き込みを高速化したい→orjson
- 読み込みを高速化したい→pysimdjson, orjson
JSONライブラリ調査結果 by ChatGPT 4o (コピペ)
Pythonで大規模なJSONファイルを高速に読み込むためのライブラリについて調査しました。以下に、主要なライブラリの特徴と選定のポイントをまとめます。
🔍 高速なJSON読み込みライブラリの比較
1. orjson
- 特徴:Rustで実装された高速なJSONライブラリ。Pythonの標準
jsonライブラリよりも高速で、dataclass、datetime、numpy、UUIDなどの型をネイティブにサポートします。 - パフォーマンス:ベンチマークによると、
orjsonはjsonの約6倍、ujsonの約2倍の速度で動作します。 - 制限事項:
orjsonのdumps関数はバイト列を返すため、文字列が必要な場合はデコードが必要です。
2. ujson (UltraJSON)
- 特徴:Cで実装された高速なJSONエンコーダー/デコーダー。Pythonの標準
jsonライブラリの代替として簡単に導入できます - パフォーマンス:
jsonよりも約3倍高速です。 - 制限事項:一部のJSON仕様に完全には準拠していない場合があります。
3. msgspec
- 特徴:JSONのエンコード/デコードとスキーマ検証を高速に行うライブラリ。型ヒントを活用して、データのバリデーションとパースを同時に行えます
- パフォーマンス:特に型ヒントを使用したデコードでは、他のライブラリよりも高速です。
- 制限事項:型ヒントの定義が必要で、柔軟性に欠ける場合があります。
4. cysimdjson
- 特徴:C++の
simdjsonライブラリをCythonでラップしたPythonバインディング。大規模なJSONファイルのパースに特化しています。 - パフォーマンス:標準の
jsonライブラリよりも7〜12倍高速です。 - 制限事項:出力は読み取り専用の辞書風オブジェクトで、通常のPython辞書に変換すると速度の利点が失われます。
🧠 ライブラリ選定のポイント
| 使用目的 | 推奨ライブラリ | 理由 |
|---|---|---|
| 高速な読み込みと書き込み | orjson | 全体的なパフォーマンスが高く、広範な型サポートがあるため。 |
| 型検証と高速デコード | msgspec | 型ヒントを活用したデコードが高速で、スキーマ検証も可能。 |
| 大規模ファイルの高速読み込み | cysimdjson | 大規模なJSONファイルの読み込みに最適化されている。 |
| 標準ライブラリの代替としての高速化 | ujson | 導入が簡単で、標準jsonよりも高速。 |
ベンチマーク結果
ChatGPT 4o環境では msgspecと pysimdjsonの動作検証ができなかったため、生成させたベンチマーク用プログラムを用いて読み書き時間を測定しました。
また、本ベンチマークではcysimdjsonの代わりに、よりgithubのスター数が多いpysimdjsonを採用しました。simdjsonベースのライブラリは読み込み特化なので書き込みは計測不能 (NaN)です。
Library json msgspec orjson simdjson ujson
File Type Operation
dict_of_lists read 0.8672 0.4079 0.4193 0.2004 0.5748
write 2.5858 0.3413 0.2905 NaN 0.6272
flat read 1.6222 1.1668 1.0851 0.3606 1.3167
write 4.5274 0.4437 0.4292 NaN 1.0326
list_of_dicts read 1.5325 0.7963 0.7550 0.3271 1.2265
write 5.0883 0.4648 0.4264 NaN 1.0072
mixed read 3.9735 2.2704 2.3687 0.8405 2.9508
write 17.9492 0.9813 0.9352 NaN 2.4250
nested read 0.7871 0.3633 0.4175 0.1960 0.5363
write 7.2655 0.3258 0.3226 NaN 0.6309Python標準のjsonライブラリとサードパーティーライブラリの間には大きな差があることがよくわかります。
サードパーティーライブラリ間の差は極端に大きいわけではないようです。総合的に見ると、ファイル読み込みにはpysimdjson、書き込みにはorjsonを使うと良さそうです。
ベンチマークに使用したプログラム by ChatGPT 4o
ChatGPTに生成させたコードがそのままでは動かなかったので、適宜修正したプログラムを貼ります。主にpysimdjsonの使い方が間違っていました。
import json
import orjson
import ujson
import msgspec
import simdjson
import time
import os
import random
from tqdm import tqdm
from typing import Any, Callable
# === データ生成部分 ===
def random_primitive():
return random.choice([
random.randint(0, 100),
random.uniform(0, 100),
random.choice(["foo", "bar", "baz"]),
random.choice([True, False]),
None
])
def generate_flat_record(fields=15_000):
return {f"field_{i}": random_primitive() for i in range(fields)}
def generate_nested_record(depth=10, list_size=15_000):
def nest(level):
if level == 0:
return [random_primitive() for _ in range(list_size)]
return {f"level_{level}": nest(level - 1)}
return nest(depth)
def generate_list_of_dicts(list_length=500, dict_fields=30):
return [generate_flat_record(dict_fields) for _ in range(list_length)]
def generate_dict_of_lists(keys=500, list_size=30):
return {f"key_{i}": [random_primitive() for _ in range(list_size)] for i in range(keys)}
def save_json(data: Any, filename: str):
# with open(filename, "w", encoding="utf-8") as f:
# json.dump(data, f)
with open(filename, "wb") as f:
f.write(orjson.dumps(data))
def generate_all(output_dir="varied_json", num_records=1_000):
args_1 = 10_000
args_1_2 = [50, args_1]
args_2 = [100, 100]
os.makedirs(output_dir, exist_ok=True)
flat = [generate_flat_record(args_1) for _ in tqdm(range(num_records))]
save_json(flat, os.path.join(output_dir, "flat.json"))
nested = [generate_nested_record(*args_1_2) for _ in tqdm(range(num_records))]
save_json(nested, os.path.join(output_dir, "nested.json"))
list_of_dicts = [generate_list_of_dicts(*args_2) for _ in tqdm(range(num_records))]
save_json(list_of_dicts, os.path.join(output_dir, "list_of_dicts.json"))
dict_of_lists = [generate_dict_of_lists(*args_2) for _ in tqdm(range(num_records))]
save_json(dict_of_lists, os.path.join(output_dir, "dict_of_lists.json"))
mixed = []
for _ in tqdm(range(num_records)):
mixed.append({
"id": random.randint(1, args_1),
"meta": generate_flat_record(args_1),
"config": generate_nested_record(*args_1_2),
"history": generate_list_of_dicts(*args_2)
})
save_json(mixed, os.path.join(output_dir, "mixed.json"))
# === ベンチマーク部分 ===
INPUT_DIR = "varied_json"
OUTPUT_DIR = "json_outputs"
os.makedirs(OUTPUT_DIR, exist_ok=True)
FILE_TYPES = ["flat", "nested", "list_of_dicts", "dict_of_lists", "mixed"]
def read_with_json(path):
with open(path, "r", encoding="utf-8") as f: return json.load(f)
def read_with_orjson(path):
with open(path, "rb") as f: return orjson.loads(f.read())
def read_with_ujson(path):
with open(path, "r", encoding="utf-8") as f: return ujson.load(f)
def read_with_msgspec(path):
with open(path, "rb") as f: return msgspec.json.decode(f.read())
def read_with_simdjson(path):
parser = simdjson.Parser()
with open(path, "r", encoding="utf-8") as f: return parser.parse(f.read())
def write_with_json(data, path):
with open(path, "w", encoding="utf-8") as f: json.dump(data, f)
def write_with_orjson(data, path):
with open(path, "wb") as f: f.write(orjson.dumps(data))
def write_with_ujson(data, path):
with open(path, "w", encoding="utf-8") as f: ujson.dump(data, f)
def write_with_msgspec(data, path):
with open(path, "wb") as f: f.write(msgspec.json.encode(data))
def benchmark(label: str, func: Callable, *args) -> float:
start = time.time()
func(*args)
return round(time.time() - start, 4)
def run_benchmark():
generate_all()
results = []
for file_type in FILE_TYPES:
file_path = os.path.join(INPUT_DIR, f"{file_type}.json")
print(f"\n📂 Benchmarking: {file_type}.json")
# 読み込み
for lib, func in [
("json", read_with_json),
("orjson", read_with_orjson),
("ujson", read_with_ujson),
("msgspec", read_with_msgspec),
("simdjson", read_with_simdjson),
]:
try:
t = benchmark(lib, func, file_path)
results.append((file_type, "read", lib, t))
except:
results.append((file_type, "read", lib, None))
# 書き込み
try:
data = read_with_json(file_path)
for lib, func in [
("json", write_with_json),
("orjson", write_with_orjson),
("ujson", write_with_ujson),
("msgspec", write_with_msgspec),
]:
out_path = os.path.join(OUTPUT_DIR, f"{file_type}_{lib}.json")
try:
t = benchmark(lib, func, data, out_path)
results.append((file_type, "write", lib, t))
except:
results.append((file_type, "write", lib, None))
except:
for lib in ["json", "orjson", "ujson", "msgspec"]:
results.append((file_type, "write", lib, None))
return results
if __name__ == "__main__":
result_data = run_benchmark()
import pandas as pd
df = pd.DataFrame(result_data, columns=["File Type", "Operation", "Library", "Time (s)"])
df_pivot = df.pivot(index=["File Type", "Operation"], columns="Library", values="Time (s)")
print("\n📊 Benchmark Summary:")
print(df_pivot)この記事は役に立ちましたか?
もし参考になりましたら、下記のボタンで教えてください。
コメント