Pythonから使えるJSONライブラリの速度比較

大きなJSONファイルの読み書きをする必要があったので、Pythonから使える高速なJSONライブラリのベンチマークを行いました。比較対象のライブラリはChatGPT 4oを使って調査しました。

この記事の結論

JSONライブラリ調査結果 by ChatGPT 4o (コピペ)

Pythonで大規模なJSONファイルを高速に読み込むためのライブラリについて調査しました。以下に、主要なライブラリの特徴と選定のポイントをまとめます。


🔍 高速なJSON読み込みライブラリの比較

1. orjson

  • 特徴:Rustで実装された高速なJSONライブラリ。Pythonの標準jsonライブラリよりも高速で、dataclassdatetimenumpyUUIDなどの型をネイティブにサポートします。
  • パフォーマンス:ベンチマークによると、orjsonjsonの約6倍、ujsonの約2倍の速度で動作します。
  • 制限事項:orjsondumps関数はバイト列を返すため、文字列が必要な場合はデコードが必要です。

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環境では msgspecpysimdjsonの動作検証ができなかったため、生成させたベンチマーク用プログラムを用いて読み書き時間を測定しました。

また、本ベンチマークでは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.6309

Python標準の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)

この記事は役に立ちましたか?

もし参考になりましたら、下記のボタンで教えてください。

関連記事

コメント

この記事へのコメントはありません。

CAPTCHA