Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

json: Optimize escaping string in Encoder#133186

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
methane wants to merge6 commits intopython:main
base:main
Choose a base branch
Loading
frommethane:optimize-json-encode

Conversation

methane
Copy link
Member

No description provided.

@methanemethane added performancePerformance or resource usage skip issue extension-modulesC modules in the Modules dir labelsApr 30, 2025
@methanemethane requested a review fromCopilotApril 30, 2025 07:33
Copilot

This comment was marked as resolved.

@methane
Copy link
MemberAuthor

without--enable-optimizations:

https://github.com/python/pyperformance/blob/main/pyperformance/data-files/benchmarks/bm_json_dumps/run_benchmark.py

Mean +- std dev: [main] 9.25 ms +- 0.07 ms -> [patched] 7.68 ms +- 0.03 ms: 1.20x faster

@mdboom
Copy link
Contributor

I'm going to benchmark this on pyperformance on the Faster CPython infrastructure and report back in a couple of hours.

@nineteendo
Copy link
Contributor

I benchmarked this feature on my own library and I'm a bit worried. Strings without escapes are faster, but strings with escapes are a lot slower:

encodejson (setuptools)jsonyx (2.2.1)reference time
List of 256 ASCII strings1.00x0.89x49.97 μs
List of 256 dicts with 1 int1.00x1.02x90.40 μs
Medium complex object1.00x1.06x138.32 μs
List of 256 strings1.00x0.91x310.31 μs
Complex object1.00x0.99x1522.59 μs
Dict with 256 lists of 256 dicts with 1 int1.00x1.07x23563.12 μs
encodejson (setuptools)jsonyx (main)reference time
List of 256 ASCII strings1.00x0.47x66.49 μs
List of 256 dicts with 1 int1.00x0.94x94.91 μs
Medium complex object1.00x0.91x146.82 μs
List of 256 strings1.00x2.76x323.10 μs
Complex object1.00x1.26x1523.92 μs
Dict with 256 lists of 256 dicts with 1 int1.00x0.92x22958.90 μs

@methane
Copy link
MemberAuthor

methane commentedApr 30, 2025 via email
edited
Loading

How about adding if (copy_len > 0) before PyUnicodeWriter_WriteSubstring?

@nineteendo
Copy link
Contributor

Better, but it's still twice as slow:

encodejson (setuptools)jsonyxreference time
List of 256 ASCII strings1.00x0.60x50.39 μs
List of 256 dicts with 1 int1.00x0.92x91.32 μs
Medium complex object1.00x0.87x144.80 μs
List of 256 strings1.00x2.05x305.92 μs
Complex object1.00x1.15x1543.54 μs
Dict with 256 lists of 256 dicts with 1 int1.00x0.91x23013.43 μs

@nineteendo
Copy link
Contributor

nineteendo commentedApr 30, 2025
edited
Loading

How about just writing strings without escapes directly to the unicode writer?
Because the main performance improvement of this PR is simply to avoid creating a new string.

_PyUnicodeWriter_WriteChar(writer,'"')_PyUnicodeWriter_WriteStr(writer,pystr)// original string_PyUnicodeWriter_WriteChar(writer,'"')

@nineteendo
Copy link
Contributor

nineteendo commentedApr 30, 2025
edited
Loading

Results of that (nineteendo/jsonyx@7c31ee4):

encodejson (setuptools)jsonyx (main)reference time
List of 256 ASCII strings1.00x0.45x50.34 μs
List of 256 dicts with 1 int1.00x0.86x91.83 μs
Medium complex object1.00x0.86x141.84 μs
List of 256 strings1.00x0.97x313.86 μs
Complex object1.00x1.03x1529.10 μs
Dict with 256 lists of 256 dicts with 1 int1.00x0.86x23190.66 μs

It's going to be a little harder to apply the change here (unless we just duplicate the functions).

@nineteendo
Copy link
Contributor

I would still like a proper fix forfaster-cpython/ideas#726 though. Should we just switch back to the private API?

@methanemethaneforce-pushed theoptimize-json-encode branch from646f257 to5c8fcf9CompareMay 1, 2025 03:57
@methanemethaneforce-pushed theoptimize-json-encode branch from5c8fcf9 to8e5e00bCompareMay 1, 2025 06:17
@nineteendo
Copy link
Contributor

See#133239 for my approach.

@methanemethaneforce-pushed theoptimize-json-encode branch fromb66863d to19c0f1fCompareMay 1, 2025 08:01
@methane
Copy link
MemberAuthor

https://gist.github.com/methane/e080ec9783db2a313f40a2b9e1837e72

Benchmarkmainpatched2
json_dumps: List of 256 booleans16.6 us16.5 us: 1.01x faster
json_dumps: List of 256 ASCII strings67.9 us34.7 us: 1.96x faster
json_dumps: List of 256 dicts with 1 int122 us101 us: 1.21x faster
json_dumps: Medium complex object205 us173 us: 1.18x faster
json_dumps: List of 256 strings330 us302 us: 1.09x faster
json_dumps: Complex object2.57 ms1.96 ms: 1.31x faster
json_dumps: Dict with 256 lists of 256 dicts with 1 int30.5 ms26.5 ms: 1.15x faster
json_dumps(ensure_ascii=False): List of 256 booleans16.6 us16.5 us: 1.01x faster
json_dumps(ensure_ascii=False): List of 256 ASCII strings68.1 us34.6 us: 1.96x faster
json_dumps(ensure_ascii=False): List of 256 dicts with 1 int122 us101 us: 1.21x faster
json_dumps(ensure_ascii=False): Medium complex object205 us172 us: 1.19x faster
json_dumps(ensure_ascii=False): List of 256 strings329 us303 us: 1.09x faster
json_dumps(ensure_ascii=False): Complex object2.56 ms1.95 ms: 1.31x faster
json_dumps(ensure_ascii=False): Dict with 256 lists of 256 dicts with 1 int30.6 ms26.5 ms: 1.15x faster
json_loads: List of 256 booleans9.01 us9.09 us: 1.01x slower
json_loads: List of 256 ASCII strings40.7 us40.2 us: 1.01x faster
json_loads: List of 256 floats91.4 us88.3 us: 1.03x faster
json_loads: Medium complex object150 us147 us: 1.02x faster
json_loads: List of 256 strings848 us816 us: 1.04x faster
json_loads: Dict with 256 lists of 256 dicts with 1 int46.5 ms46.7 ms: 1.00x slower
json_loads: List of 256 stringsensure_ascii=False85.2 us85.7 us: 1.01x slower
Geometric mean(ref)1.13x faster

Benchmark hidden because not significant (5): json_dumps: List of 256 floats, json_dumps(ensure_ascii=False): List of 256 floats, json_loads: List of 256 dicts with 1 int, json_loads: Complex object, json_loads: Complex objectensure_ascii=False

@methane
Copy link
MemberAuthor

methane commentedMay 1, 2025
edited
Loading

This PR is faster, but#133239 is enough for fixing regression from Python 3.13.

For longer term, encoder should use private (maybe utf-8) buffer instead of PyUnicodeWriter.
Calling overhead of PyUnicodeWriter is not negligible. It is enough for "much faster than pure Python", but not enough for JSON serializer.

@nineteendo
Copy link
Contributor

This PR is faster, but#133239 is enough for fixing regression from Python 3.13.

It's still not fully fixed, encoding booleans is twice as slow. And I don't fully understand why this PR is faster.

@mdboom
Copy link
Contributor

Just as a data point, on our Faster CPython infrastructure, this makes the json_dumps benchmark14.8% faster than main, and is within thenoise as the same performance as 3.13.0.

I will also kick off a run on#133239 for comparison.

nineteendo reacted with thumbs up emoji

@methanemethane requested a review fromvstinnerMay 2, 2025 06:03
@methane
Copy link
MemberAuthor

methane commentedMay 2, 2025
edited by hugovk
Loading

Using_PyUnicodeWriter_WriteASCIIString() instead ofPyUnicodeWriter_WriteUTF8:

$./python -m pyperf compare_to with-fast-path.json use_write_ascii.json -GSlower (3):- json_dumps(ensure_ascii=False): List of 256 dicts with 1 int: 101 us +- 0 us -> 102 us +- 0 us: 1.00x slower- json_loads: Dict with 256 lists of 256 dicts with 1 int: 46.6 ms +- 0.1 ms -> 46.8 ms +- 0.5 ms: 1.00x slower- json_dumps(ensure_ascii=False): List of 256 floats: 239 us +- 1 us -> 239 us +- 1 us: 1.00x slowerFaster (10):- json_dumps(ensure_ascii=False): List of 256 strings: 303 us +- 5 us -> 279 us +- 3 us: 1.08x faster- json_dumps: List of 256 strings: 302 us +- 3 us -> 278 us +- 3 us: 1.08x faster- json_dumps(ensure_ascii=False): List of 256 booleans: 16.5 us +- 0.1 us -> 15.3 us +- 0.1 us: 1.08x faster- json_dumps: List of 256 booleans: 16.5 us +- 0.1 us -> 15.3 us +- 0.1 us: 1.07x faster- json_dumps: Complex object: 1.96 ms +- 0.01 ms -> 1.87 ms +- 0.01 ms: 1.05x faster- json_dumps(ensure_ascii=False): Complex object: 1.96 ms +- 0.01 ms -> 1.87 ms +- 0.02 ms: 1.05x faster- json_dumps: Medium complex object: 173 us +- 1 us -> 171 us +- 1 us: 1.01x faster- json_dumps(ensure_ascii=False): Medium complex object: 172 us +- 1 us -> 171 us +- 1 us: 1.01x faster- json_loads: Medium complex object: 148 us +- 1 us -> 147 us +- 1 us: 1.00x faster- json_dumps: List of 256 floats: 239 us +- 0 us -> 239 us +- 0 us: 1.00x fasterBenchmark hidden because not significant (13): json_dumps: List of 256 ASCII strings, json_dumps: List of 256 dicts with 1 int, json_dumps: Dict with 256 lists of 256 dicts with 1 int, json_dumps(ensure_ascii=False): List of 256 ASCII strings, json_dumps(ensure_ascii=False): Dict with 256 lists of 256 dicts with 1 int, json_loads: List of 256 booleans, json_loads: List of 256 ASCII strings, json_loads: List of 256 floats, json_loads: List of 256 dicts with 1 int, json_loads: List of 256 strings, json_loads: Complex object, json_loads: List of 256 stringsensure_ascii=False, json_loads: Complex objectensure_ascii=False

Patch:

diff --git a/Modules/_json.c b/Modules/_json.cindex cd08fa688d3..cd57760282a 100644--- a/Modules/_json.c+++ b/Modules/_json.c@@ -351,7 +351,7 @@ write_escaped_ascii(PyUnicodeWriter *writer, PyObject *pystr)         }         if (buf_len + 12 > ESCAPE_BUF_SIZE) {-            ret = PyUnicodeWriter_WriteUTF8(writer, buf, buf_len);+            ret = _PyUnicodeWriter_WriteASCIIString((_PyUnicodeWriter*)writer, buf, buf_len);             if (ret) return ret;             buf_len = 0;         }@@ -359,7 +359,7 @@ write_escaped_ascii(PyUnicodeWriter *writer, PyObject *pystr)     assert(buf_len < ESCAPE_BUF_SIZE);     buf[buf_len++] = '"';-    return PyUnicodeWriter_WriteUTF8(writer, buf, buf_len);+    return _PyUnicodeWriter_WriteASCIIString((_PyUnicodeWriter*)writer, buf, buf_len); } static int@@ -1612,13 +1612,13 @@ encoder_listencode_obj(PyEncoderObject *s, PyUnicodeWriter *writer,     int rv;     if (obj == Py_None) {-      return PyUnicodeWriter_WriteUTF8(writer, "null", 4);+      return _PyUnicodeWriter_WriteASCIIString((_PyUnicodeWriter*)writer, "null", 4);     }     else if (obj == Py_True) {-      return PyUnicodeWriter_WriteUTF8(writer, "true", 4);+      return _PyUnicodeWriter_WriteASCIIString((_PyUnicodeWriter*)writer, "true", 4);     }     else if (obj == Py_False) {-      return PyUnicodeWriter_WriteUTF8(writer, "false", 5);+      return _PyUnicodeWriter_WriteASCIIString((_PyUnicodeWriter*)writer, "false", 5);     }     else if (PyUnicode_Check(obj)) {         return encoder_write_string(s, writer, obj);@@ -1779,7 +1779,7 @@ encoder_listencode_dict(PyEncoderObject *s, PyUnicodeWriter *writer,     if (PyDict_GET_SIZE(dct) == 0) {         /* Fast path */-        return PyUnicodeWriter_WriteUTF8(writer, "{}", 2);+        return _PyUnicodeWriter_WriteASCIIString((_PyUnicodeWriter*)writer, "{}", 2);     }     if (s->markers != Py_None) {@@ -1883,7 +1883,7 @@ encoder_listencode_list(PyEncoderObject *s, PyUnicodeWriter *writer,         return -1;     if (PySequence_Fast_GET_SIZE(s_fast) == 0) {         Py_DECREF(s_fast);-        return PyUnicodeWriter_WriteUTF8(writer, "[]", 2);+        return _PyUnicodeWriter_WriteASCIIString((_PyUnicodeWriter*)writer, "[]", 2);     }     if (s->markers != Py_None) {

@methane
Copy link
MemberAuthor

May I merge this PR before Python 3.14b1 release?

@vstinner How do you think about using_PyUnicodeWriter_WriteASCIIString like this?#133186 (comment)

@vstinner
Copy link
Member

May I merge this PR before Python 3.14b1 release?

You missed the feature freeze, this change should now target Python 3.15.

@vstinner How do you think about using _PyUnicodeWriter_WriteASCIIString like this?#133186 (comment)

That's perfectly fine for a stdlib module, especially if it's faster :-)

----

* Improve the performance of :class:`~json.JSONEncoder` encodes strings.
(Contributed by Inada Naoki in :gh:`133186`.)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

You should now retarget this change to Python 3.15 (move the text to Doc/whatsnew/3.15.rst).

@nineteendo
Copy link
Contributor

I found the difference between our PRs: you're usingPyUnicodeWriter_WriteSubstring(), while I'm usingPyUnicodeWriter_WriteStr()in_steal_accumulate(). The latter creates a new string by callingPyObject_Str(), while the former does not.

@vstinner
Copy link
Member

I found the difference between our PRs: you're using PyUnicodeWriter_WriteSubstring(), while I'm using PyUnicodeWriter_WriteStr() in _steal_accumulate(). The latter creates a new string by calling PyObject_Str(), while the former does not.

PyObject_Str() doesn't create a new string.

@nineteendo
Copy link
Contributor

nineteendo commentedMay 16, 2025
edited
Loading

PyObject_Str() doesn't create a new string.

I see. IsPyErr_CheckSignals() orPy_NewRef() maybe expensive to call? Because there has to be an explanation.

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@vstinnervstinnervstinner left review comments

@nineteendonineteendonineteendo left review comments

Copilot code reviewCopilotCopilot left review comments

Assignees
No one assigned
Labels
awaiting core reviewextension-modulesC modules in the Modules dirperformancePerformance or resource usageskip issue
Projects
None yet
Milestone
No milestone
Development

Successfully merging this pull request may close these issues.

4 participants
@methane@mdboom@nineteendo@vstinner

[8]ページ先頭

©2009-2025 Movatter.jp