| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
Add a fast path to _match_number_unicode for integers that fit in a 64-bit integer (at most 19 decimal digits): accumulate the value directly into an unsigned long long instead of allocating a PyBytes and calling the generic PyLong_FromString. Positive values use PyLong_FromUnsignedLongLong; negatives within long long range use PyLong_FromLongLong; larger integers fall back to the previous path. For floats and big integers, copy the (always-ASCII) number text into a stack buffer for the common short case to avoid the PyBytes allocation, and call PyOS_string_to_double directly for floats. Benchmarks (optimized free-threaded build): * pyperformance json_loads: 1.06x faster overall * microbench: small int arrays ~2x, 20-int doc 1.48x, mixed dict 1.16x All test_json tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Looks good, but the wide scope of numstr makes it a bit hard to reason about refleaks.
Can you remove the early declaration of numstr and define it where you need it.
Sorry, something went wrong.
| } | ||
| else | ||
| rval = PyLong_FromString(buf, NULL, 10); | ||
| Py_XDECREF(numstr); |
There was a problem hiding this comment.
numstr is only defined on lines 1068 and 1108.
You could declare it only in those places, and decref it in the same blocks.
Sorry, something went wrong.
| *next_idx_ptr = idx; | ||
| rval = PyLong_FromUnsignedLongLong(value); | ||
| if (neg && rval != NULL) { | ||
| Py_SETREF(rval, PyNumber_Negative(rval)); |
There was a problem hiding this comment.
this can use _PyLong_Negate
Sorry, something went wrong.
There was a problem hiding this comment.
_PyLong_Negate is define in longobject.c and not part of the internal API. I do not think it is worth it here to expose.
If we change idx - start - neg <= 19 into idx - start - neg <= 18 we can fit value into a signed long and negate value (but we loose the fast path for values with 19 digits). There are a few more tricks we could do, but I think the performance gain would not be enough to warrant the extra complexity.
Sorry, something went wrong.
There was a problem hiding this comment.
I implemented it like this:
n = idx - start;
if (!is_float && n <= 19) {
if (PyUnicode_READ(kind, str, start) == '-') {
/* integer with at most 18 digits (excluding sign) fits in
long long (10 ** 18 - 1 <= 2 ** 63 < 10 ** 19 - 1) */
long long value = 0;
for (i = start + 1; i < idx; i++) {
value = value * 10 - (PyUnicode_READ(kind, str, i) - '0');
}
return PyLong_FromLongLong(value);
}
else {
/* integer with at most 19 digits fits in unsigned long
long (10 ** 19 - 1 <= 2 ** 64 - 1 < 10 ** 20 - 1) */
unsigned long long value = 0;
for (i = start; i < idx; i++) {
value = value * 10 + (PyUnicode_READ(kind, str, i) - '0');
}
return PyLong_FromUnsignedLongLong(value);
}
}
Sorry, something went wrong.
There was a problem hiding this comment.
Nice! If we have - we are guaranteed to have one digit less, so we fit into a long long.
Sorry, something went wrong.
# Conflicts: # Lib/test/test_json/test_decode.py
There was a problem hiding this comment.
Reviewed c14d8063e303475af844b3e291c9622b023bd036 against merge base 7aec160315a6316f7391abda10d4b6c57032d105.
No correctness regression reproduced in 5,252 C-versus-Python-scanner cases, 24 exact-token custom-hook checks, and 400 allocation-failure offsets. These cover 19/20-digit and signed/unsigned boundaries, negative zero, subnormal/overflow cases, exponent extremes, long buffers, 1/2/4-byte Unicode inputs and conversion limits. The shared debug-runtime comparison passes 237 JSON tests (3 skipped), plus all 46 tests in this PR's exact test_decode module on both variants.
I compared the two overlapping proposals using one PGO/LTO core built from #156897's head. The baseline _json.c sources are identical; each of baseline, #156897 and #150639 was compiled with the same -O3 flags and headers, without module-specific PGO. This isolates the decoder components:
| Workload | base | 156897 | 150639 |
|---|---|---|---|
| float_array_10000 | 1.96 ms | 1.89 ms | 1.75 ms |
| numeric_records_2000 | 1.65 ms | 1.63 ms | 1.52 ms |
| pyperformance_json_loads | 20.00 µs | 19.88 µs | 19.00 µs |
Inputs: 10,000 successive random() calls on one Random(156897) instance (the float-array workload from #156897's discussion, with a fixed seed); 2,000 records with integer IDs, two floats and a boolean; and the three-object pyperformance json_loads fixture. The latter is timed with its original 20-call unrolling and normalization. #156897 still allocates the intermediate character buffer; #150639 additionally provides integer and stack-buffer fast paths.
Timings are medians from seven interleaved fresh processes on Linux x86-64, i9-9900K/GCC 16.2.1, pinned to one logical CPU; GC disabled during timed loops, hash seed 0, calibration to 65 ms followed by a separately timed batch. Builds and tests had finished before timing; the desktop was not frequency-isolated. These are workload-specific measurements, not application-wide speedups.
Review and testing performed with Codex (AI assistance).
Sorry, something went wrong.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Avoids the PyNumber_Negative call: a negative literal of at most 19 characters has at most 18 digits, which always fits in a long long. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
@kumaraditya303 Could you review again? I made two minor updates: the suggestion by nineteendo to avoid to negation and avoid mutation of bytes objects. |
Sorry, something went wrong.
|
Could you credit me? Co-authored-by: Nice Zombies <nineteendo19d0@gmail.com> Here are some benchmark results for this optimisation in jsonyx (let me know if you want to test more cases):
Footnotes |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
_match_number_unicode() (the C accelerator behind json.loads) previously allocated a PyBytes object for every number, copied the digits into it, and then called the generic PyLong_FromString / PyFloat_FromString parsers.
This PR parses the common cases directly from the already-scanned text.
The standard bm_json_loads document is string/dict-dominated, so it gains
less.
Also addresses #155742
Benchmark script