python / python/cpython

Speed up JSON string decoding for documents with long string values

未關閉
#150,871 0 則留言 0 個 reaction 已指派 0 人 在 GitHub 檢視

還沒有人認領這個 Issue。

extension-modules performance type-feature
主要語言
Python
星號
77.2k
分支
35.9k
PR 合併指標
PR 指標待擷取

描述

Proposal

The C JSON decoder finds the end of every string by reading one character at a time, checking each for the closing quote, a backslash, or a control character (scanstring_unicode in Modules/_json.c). For a string with a long run of ordinary characters, such as a log line, a text field, or a base64 or embedded-document value, that loop does one read and several comparisons per byte while the machine's 64-bit registers sit mostly idle.

The proposal is to scan the one-byte (ASCII/Latin-1) representation eight bytes at a time: load eight bytes into a single machine word and test all eight at once for the closing quote, a backslash, or a control character. A run of ordinary characters then advances eight bytes per step. At the first byte that needs attention, the existing per-character loop takes over, so every decode decision stays on the current path. Two-byte and four-byte strings (anything containing a non-Latin-1 character) keep the current loop unchanged.

In one line: today the decoder asks "is this byte special?" once per byte; this asks "is any of these eight bytes special?" once per eight, and drops to per-byte only when the answer is yes.

How this differs from the SIMD backend in #142915

This is not the SIMD parsing architecture declined in #142915. It uses no SIMD intrinsics, no runtime CPU detection, and no build configuration. It relies only on portable 64-bit integer arithmetic, with the same 0x0101… / 0x8080… masks that Objects/unicodeobject.c already applies for ASCII scanning. It changes one function and adds no infrastructure, so it does not depend on #125022 and needs no PEP.

The single-character find_char from fastsearch.h (adopted for the SRE prefix scanner in #148729) does not fit here: a JSON string scan stops at the first of three different bytes, and the strict-mode control-character test is a character-class check that a single-character search cannot express. A word-at-a-time mask handles the class in one operation.

When it helps, and when it does not

Measured json.loads speedups against the current decoder:

Document shape Effect
One long text field (~11 KB string) 6.3x faster
Many 200-character ASCII string values 4.5x faster
Realistic mixed records (short and medium strings) 1.17x faster
Short keys, numbers, the pyperformance document no change
Strings with emoji or other non-Latin-1 text no change (scalar path)

The standard pyperformance bm_json_loads document is short-string and dict dominated, so it shows no change. The benefit is specific to documents whose payload is long text.

Correctness

The decoded output is byte-identical to the current decoder. A proof-of-concept patch is validated against test_json, a 347-input differential corpus (real-world JSON plus a special character placed at every offset across the eight-byte window, in all three string representations), and all 340 files of nst/JSONTestSuite (318 parsing plus 22 transform). Every value and every error position matches.

Relation to other json work

Independent of, and complementary to, the active number-parsing (#150639) and encoder (#150827) changes.

A proof-of-concept PR follows.

Benchmark

Built base and patched interpreters from this branch's main ancestor and the patch, ran the same script under each, and compared with pyperf compare_to (A/B by swapping Lib/json/decoder.py on the same build; macOS arm64, non-PGO).

import json, pyperf
# Ceiling probe: vary string length & kind to expose where SWAR string-scan helps.
long_ascii   = json.dumps([("x"*200) for _ in range(200)])          # long ASCII values -> max win
text_blob    = json.dumps({"body": "lorem ipsum dolor sit amet " * 400})  # one huge string
short_keys   = json.dumps({f"k{i}": i for i in range(2000)})         # short keys -> minimal win
mixed_real   = json.dumps([{"id":i,"name":f"user_{i}","email":f"u{i}@example.com","bio":"hello "*10} for i in range(300)])
multikind    = json.dumps(["emoji 😀 中文 текст "*20 for _ in range(200)])  # UCS-2/4 -> scalar fallback (neutral check)
# pyperformance dataset
SD={'key1':0,'key2':True,'key3':'value','key4':'foo','key5':'string'}
ND={'key1':0,'key2':SD,'key3':'value','key4':SD,'key5':SD,'key':'ąćż'}
ppset=[(json.dumps({}),2000),(json.dumps(SD),1000),(json.dumps(ND),1000),(json.dumps([ND]*1000),1)]

docs = {"long_ascii_values": long_ascii, "huge_text_blob": text_blob,
        "short_keys": short_keys, "mixed_real": mixed_real, "multikind_emoji": multikind}
runner = pyperf.Runner()
for name, s in docs.items():
    runner.bench_func(f"loads/{name}", lambda s=s: json.loads(s))
def pp(items):
    for s,n in items:
        for _ in range(n): json.loads(s)
runner.bench_func("loads/pyperformance", pp, ppset)
Linked PRs
  • gh-150872

貢獻指南

開啟貢獻指南

從這裡開始

  1. 先讀完整個 Issue,再讀專案的貢獻指南。
  2. 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
  3. Fork 儲存庫,在一個分支上完成修改。
  4. 送出 Pull Request,並在描述裡引用這個 Issue 編號。

研究方向

從 Modules/_json.c 中的 scanstring_unicode 開始,檢查現有的單位元組字串路徑。執行 test_json,並將結果與包含 347 個輸入的差分語料庫和 nst/JSONTestSuite 進行比較;完成的標準是解碼後的值和錯誤位置相符,同時改善此處所述的長字串基準測試案例。

由索引模型根據 Issue 內容生成。

評估

技術堆疊
c, python
領域
performance
Issue 類型
功能
難度
4/5
預估耗時
3-5 天
活躍度
停滯
描述清晰度
描述清楚
新手友好度
25/100

把新 issue 寄到你的電子郵件信箱

精選適合新手參與的 GitHub issue 摘要。