fastsimdjson 0.6.0__tar.gz → 0.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastsimdjson-0.6.0/src/fastsimdjson.egg-info → fastsimdjson-0.7.0}/PKG-INFO +42 -6
- {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/README.md +40 -5
- {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/pyproject.toml +2 -2
- {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/src/fastsimdjson.cpp +308 -13
- {fastsimdjson-0.6.0 → fastsimdjson-0.7.0/src/fastsimdjson.egg-info}/PKG-INFO +42 -6
- {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/src/fastsimdjson.egg-info/requires.txt +1 -0
- {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/tests/test_stream.py +101 -0
- {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/MANIFEST.in +0 -0
- {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/setup.cfg +0 -0
- {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/setup.py +0 -0
- {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/src/fastsimdjson.egg-info/SOURCES.txt +0 -0
- {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/src/fastsimdjson.egg-info/dependency_links.txt +0 -0
- {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/src/fastsimdjson.egg-info/top_level.txt +0 -0
- {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/tests/test_dumps.py +0 -0
- {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/tests/test_files.py +0 -0
- {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/tests/test_lazy.py +0 -0
- {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/tests/test_loads.py +0 -0
- {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/vendor/simdjson.cpp +0 -0
- {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/vendor/simdjson.h +0 -0
- {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/vendor/simdutf.cpp +0 -0
- {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/vendor/simdutf.h +0 -0
- {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/vendor/simdutf_c.h +0 -0
- {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/vendor/zmij-LICENSE +0 -0
- {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/vendor/zmij.cc +0 -0
- {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/vendor/zmij.h +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastsimdjson
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Fast JSON parsing for Python, built on simdjson
|
|
5
5
|
Author-email: Daniel Lemire <daniel@lemire.me>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -25,6 +25,7 @@ Provides-Extra: test
|
|
|
25
25
|
Requires-Dist: pytest; extra == "test"
|
|
26
26
|
Provides-Extra: bench
|
|
27
27
|
Requires-Dist: orjson; extra == "bench"
|
|
28
|
+
Requires-Dist: jiter; extra == "bench"
|
|
28
29
|
Requires-Dist: msgspec; extra == "bench"
|
|
29
30
|
Requires-Dist: pysimdjson; extra == "bench"
|
|
30
31
|
Requires-Dist: cysimdjson; extra == "bench"
|
|
@@ -38,7 +39,8 @@ replacement for the `json` module's `loads`, `load`, `dumps` and `dump`, and
|
|
|
38
39
|
it can be over 3 times faster than the standard `json.loads` and
|
|
39
40
|
`json.dumps`. When you only need part of a document, its lazy `parse`
|
|
40
41
|
function is faster still. It also reads streams of documents (NDJSON, JSON
|
|
41
|
-
Lines)
|
|
42
|
+
Lines), on many threads with free-threaded Python, and can extract only the
|
|
43
|
+
fields you need from each document. It writes JSON in two ways: `dumps` returns the same `str` as
|
|
42
44
|
`json.dumps`, and `dumpb` returns the same `bytes` as `orjson.dumps`, as fast
|
|
43
45
|
as orjson.
|
|
44
46
|
|
|
@@ -233,6 +235,37 @@ document, and the Python objects of all of them, to the calling thread.
|
|
|
233
235
|
With the GIL (the default build of Python), the calling thread does all the
|
|
234
236
|
work: `threads` has no effect on speed.
|
|
235
237
|
|
|
238
|
+
### Only some fields: `fields=`
|
|
239
|
+
|
|
240
|
+
When you need a few values from each document, `fields` extracts them
|
|
241
|
+
without building the documents: only the values found become Python
|
|
242
|
+
objects.
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
events = fastsimdjson.loads_many(data, fields=["/type", "/payload/action"])
|
|
246
|
+
# (type, action) for each document
|
|
247
|
+
|
|
248
|
+
def count(t):
|
|
249
|
+
kind, action, language = t
|
|
250
|
+
...
|
|
251
|
+
fastsimdjson.map_many(count, data, threads=64,
|
|
252
|
+
fields=["/type", "/payload/action",
|
|
253
|
+
"/payload/pull_request/base/repo/language"])
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
`loads_many` and `map_many` accept `fields`: a JSON Pointer (RFC 6901), or a
|
|
257
|
+
list or tuple of them. Each document then becomes the value at the pointer,
|
|
258
|
+
or a tuple of the values at the pointers, in order. A value that is an
|
|
259
|
+
object or an array is built in full. A pointer that leads nowhere (a
|
|
260
|
+
missing key, an index past the end, or a step into a scalar) gives
|
|
261
|
+
`default`, `None` unless you pass another value; a JSON `null` gives `None`.
|
|
262
|
+
As with `at_pointer`, a key that appears twice in an object refers to its
|
|
263
|
+
first occurrence. `fields` cannot be combined with `lazy=True`.
|
|
264
|
+
|
|
265
|
+
On 7.6 GB of GitHub events, `map_many` with three fields ran at 2.8 GB/s on
|
|
266
|
+
one core of an Intel Xeon Gold 6548N, against 2.2 GB/s with lazy views
|
|
267
|
+
(`lazy=True`). With 64 threads, it ran at 78 GB/s (lazy views: 61 GB/s).
|
|
268
|
+
|
|
236
269
|
### `release`
|
|
237
270
|
|
|
238
271
|
`release()` frees the simdjson parser and the string caches kept by the
|
|
@@ -307,8 +340,8 @@ The suite builds an ~80 MB document and a list of 16,777,221 integers, so
|
|
|
307
340
|
give it some RAM.
|
|
308
341
|
|
|
309
342
|
The benchmark scripts need simdjson-data and the `bench` extra (orjson,
|
|
310
|
-
msgspec, pysimdjson, cysimdjson). `bench.py` times `loads` against
|
|
311
|
-
`json.loads
|
|
343
|
+
jiter, msgspec, pysimdjson, cysimdjson). `bench.py` times `loads` against
|
|
344
|
+
`json.loads`, orjson and jiter, `bench_lazy.py` times `parse` against pysimdjson and
|
|
312
345
|
cysimdjson, `bench_dumps.py` times `dumps` against `json.dumps` and `dumpb`
|
|
313
346
|
against orjson and msgspec, and `bench_many.py` times `loads_many`.
|
|
314
347
|
|
|
@@ -347,7 +380,7 @@ geometric mean over the 22 files (higher is better).
|
|
|
347
380
|
|
|
348
381
|
<picture>
|
|
349
382
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_loads_dark.png">
|
|
350
|
-
<img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_loads.png" width="80%" alt="Parsing JSON in Python: fastsimdjson loads 0.78 GB/s, orjson 0.60, msgspec 0.53, pysimdjson 0.44, cysimdjson 0.43, ujson 0.37, python-rapidjson 0.24, simplejson 0.23, json 0.22">
|
|
383
|
+
<img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_loads.png" width="80%" alt="Parsing JSON in Python: fastsimdjson loads 0.78 GB/s, orjson 0.60, msgspec 0.53, jiter 0.48, pysimdjson 0.44, cysimdjson 0.43, ujson 0.37, python-rapidjson 0.24, simplejson 0.23, json 0.22">
|
|
351
384
|
</picture>
|
|
352
385
|
|
|
353
386
|
| parser | GB/s | vs `json.loads` |
|
|
@@ -358,6 +391,7 @@ geometric mean over the 22 files (higher is better).
|
|
|
358
391
|
| ujson 6.0.0 | 0.37 | 1.66× |
|
|
359
392
|
| cysimdjson 26.27 | 0.43 | 1.96× |
|
|
360
393
|
| pysimdjson 7.0.2 | 0.44 | 1.98× |
|
|
394
|
+
| jiter 0.17.0 | 0.48 | 2.21× |
|
|
361
395
|
| msgspec 0.22.0 | 0.53 | 2.42× |
|
|
362
396
|
| orjson 3.12.0 | 0.60 | 2.74× |
|
|
363
397
|
| **fastsimdjson `loads`** | **0.78** | **3.53×** |
|
|
@@ -371,7 +405,9 @@ gain comes from pausing the garbage collector while the objects are built.
|
|
|
371
405
|
Timing each call on its own, fastsimdjson's lead over orjson is 1.33× with
|
|
372
406
|
the collector enabled and 1.22× with it disabled (geometric means); without
|
|
373
407
|
the collector, `canada.json`, `mesh.json` and `numbers.json` are ties. yyjson 4.0.6 is left out: it
|
|
374
|
-
returns wrong strings for non-ASCII text.
|
|
408
|
+
returns wrong strings for non-ASCII text. jiter (the parser of pydantic)
|
|
409
|
+
caches strings by default; turning the cache off (`cache_mode=False`) makes it
|
|
410
|
+
slower here (0.44 GB/s).
|
|
375
411
|
|
|
376
412
|
Parsing is no longer the bottleneck. simdjson alone parses these files at
|
|
377
413
|
3.1 GB/s. It accounts for about a third of the time of `loads`; the rest goes
|
|
@@ -7,7 +7,8 @@ replacement for the `json` module's `loads`, `load`, `dumps` and `dump`, and
|
|
|
7
7
|
it can be over 3 times faster than the standard `json.loads` and
|
|
8
8
|
`json.dumps`. When you only need part of a document, its lazy `parse`
|
|
9
9
|
function is faster still. It also reads streams of documents (NDJSON, JSON
|
|
10
|
-
Lines)
|
|
10
|
+
Lines), on many threads with free-threaded Python, and can extract only the
|
|
11
|
+
fields you need from each document. It writes JSON in two ways: `dumps` returns the same `str` as
|
|
11
12
|
`json.dumps`, and `dumpb` returns the same `bytes` as `orjson.dumps`, as fast
|
|
12
13
|
as orjson.
|
|
13
14
|
|
|
@@ -202,6 +203,37 @@ document, and the Python objects of all of them, to the calling thread.
|
|
|
202
203
|
With the GIL (the default build of Python), the calling thread does all the
|
|
203
204
|
work: `threads` has no effect on speed.
|
|
204
205
|
|
|
206
|
+
### Only some fields: `fields=`
|
|
207
|
+
|
|
208
|
+
When you need a few values from each document, `fields` extracts them
|
|
209
|
+
without building the documents: only the values found become Python
|
|
210
|
+
objects.
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
events = fastsimdjson.loads_many(data, fields=["/type", "/payload/action"])
|
|
214
|
+
# (type, action) for each document
|
|
215
|
+
|
|
216
|
+
def count(t):
|
|
217
|
+
kind, action, language = t
|
|
218
|
+
...
|
|
219
|
+
fastsimdjson.map_many(count, data, threads=64,
|
|
220
|
+
fields=["/type", "/payload/action",
|
|
221
|
+
"/payload/pull_request/base/repo/language"])
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`loads_many` and `map_many` accept `fields`: a JSON Pointer (RFC 6901), or a
|
|
225
|
+
list or tuple of them. Each document then becomes the value at the pointer,
|
|
226
|
+
or a tuple of the values at the pointers, in order. A value that is an
|
|
227
|
+
object or an array is built in full. A pointer that leads nowhere (a
|
|
228
|
+
missing key, an index past the end, or a step into a scalar) gives
|
|
229
|
+
`default`, `None` unless you pass another value; a JSON `null` gives `None`.
|
|
230
|
+
As with `at_pointer`, a key that appears twice in an object refers to its
|
|
231
|
+
first occurrence. `fields` cannot be combined with `lazy=True`.
|
|
232
|
+
|
|
233
|
+
On 7.6 GB of GitHub events, `map_many` with three fields ran at 2.8 GB/s on
|
|
234
|
+
one core of an Intel Xeon Gold 6548N, against 2.2 GB/s with lazy views
|
|
235
|
+
(`lazy=True`). With 64 threads, it ran at 78 GB/s (lazy views: 61 GB/s).
|
|
236
|
+
|
|
205
237
|
### `release`
|
|
206
238
|
|
|
207
239
|
`release()` frees the simdjson parser and the string caches kept by the
|
|
@@ -276,8 +308,8 @@ The suite builds an ~80 MB document and a list of 16,777,221 integers, so
|
|
|
276
308
|
give it some RAM.
|
|
277
309
|
|
|
278
310
|
The benchmark scripts need simdjson-data and the `bench` extra (orjson,
|
|
279
|
-
msgspec, pysimdjson, cysimdjson). `bench.py` times `loads` against
|
|
280
|
-
`json.loads
|
|
311
|
+
jiter, msgspec, pysimdjson, cysimdjson). `bench.py` times `loads` against
|
|
312
|
+
`json.loads`, orjson and jiter, `bench_lazy.py` times `parse` against pysimdjson and
|
|
281
313
|
cysimdjson, `bench_dumps.py` times `dumps` against `json.dumps` and `dumpb`
|
|
282
314
|
against orjson and msgspec, and `bench_many.py` times `loads_many`.
|
|
283
315
|
|
|
@@ -316,7 +348,7 @@ geometric mean over the 22 files (higher is better).
|
|
|
316
348
|
|
|
317
349
|
<picture>
|
|
318
350
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_loads_dark.png">
|
|
319
|
-
<img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_loads.png" width="80%" alt="Parsing JSON in Python: fastsimdjson loads 0.78 GB/s, orjson 0.60, msgspec 0.53, pysimdjson 0.44, cysimdjson 0.43, ujson 0.37, python-rapidjson 0.24, simplejson 0.23, json 0.22">
|
|
351
|
+
<img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_loads.png" width="80%" alt="Parsing JSON in Python: fastsimdjson loads 0.78 GB/s, orjson 0.60, msgspec 0.53, jiter 0.48, pysimdjson 0.44, cysimdjson 0.43, ujson 0.37, python-rapidjson 0.24, simplejson 0.23, json 0.22">
|
|
320
352
|
</picture>
|
|
321
353
|
|
|
322
354
|
| parser | GB/s | vs `json.loads` |
|
|
@@ -327,6 +359,7 @@ geometric mean over the 22 files (higher is better).
|
|
|
327
359
|
| ujson 6.0.0 | 0.37 | 1.66× |
|
|
328
360
|
| cysimdjson 26.27 | 0.43 | 1.96× |
|
|
329
361
|
| pysimdjson 7.0.2 | 0.44 | 1.98× |
|
|
362
|
+
| jiter 0.17.0 | 0.48 | 2.21× |
|
|
330
363
|
| msgspec 0.22.0 | 0.53 | 2.42× |
|
|
331
364
|
| orjson 3.12.0 | 0.60 | 2.74× |
|
|
332
365
|
| **fastsimdjson `loads`** | **0.78** | **3.53×** |
|
|
@@ -340,7 +373,9 @@ gain comes from pausing the garbage collector while the objects are built.
|
|
|
340
373
|
Timing each call on its own, fastsimdjson's lead over orjson is 1.33× with
|
|
341
374
|
the collector enabled and 1.22× with it disabled (geometric means); without
|
|
342
375
|
the collector, `canada.json`, `mesh.json` and `numbers.json` are ties. yyjson 4.0.6 is left out: it
|
|
343
|
-
returns wrong strings for non-ASCII text.
|
|
376
|
+
returns wrong strings for non-ASCII text. jiter (the parser of pydantic)
|
|
377
|
+
caches strings by default; turning the cache off (`cache_mode=False`) makes it
|
|
378
|
+
slower here (0.44 GB/s).
|
|
344
379
|
|
|
345
380
|
Parsing is no longer the bottleneck. simdjson alone parses these files at
|
|
346
381
|
3.1 GB/s. It accounts for about a third of the time of `loads`; the rest goes
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fastsimdjson"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.0"
|
|
8
8
|
description = "Fast JSON parsing for Python, built on simdjson"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -34,7 +34,7 @@ Issues = "https://github.com/simdjson/fastpysimdjson/issues"
|
|
|
34
34
|
|
|
35
35
|
[project.optional-dependencies]
|
|
36
36
|
test = ["pytest"]
|
|
37
|
-
bench = ["orjson", "msgspec", "pysimdjson", "cysimdjson"]
|
|
37
|
+
bench = ["orjson", "jiter", "msgspec", "pysimdjson", "cysimdjson"]
|
|
38
38
|
|
|
39
39
|
[tool.cibuildwheel]
|
|
40
40
|
skip = ["pp*"]
|
|
@@ -892,11 +892,10 @@ PyObject *value_at(DocumentObject *owner, size_t i) {
|
|
|
892
892
|
}
|
|
893
893
|
}
|
|
894
894
|
|
|
895
|
-
// Tape index of the value of the first field named key
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
const uint8_t *strings = owner->doc->string_buf.get();
|
|
895
|
+
// Tape index of the value of the first field named key in the object at
|
|
896
|
+
// start, or 0.
|
|
897
|
+
size_t find_key_in(const uint64_t *tape, const uint8_t *strings, size_t start,
|
|
898
|
+
const char *key, size_t len) {
|
|
900
899
|
size_t end = close_of(tape, start);
|
|
901
900
|
for (size_t i = start + 1; i < end; i = skip(tape, i + 1)) {
|
|
902
901
|
size_t idx = size_t(tape[i] & simdjson::internal::JSON_VALUE_MASK);
|
|
@@ -909,6 +908,11 @@ size_t find_key(DocumentObject *owner, size_t start, const char *key,
|
|
|
909
908
|
return 0;
|
|
910
909
|
}
|
|
911
910
|
|
|
911
|
+
size_t find_key(DocumentObject *owner, size_t start, const char *key,
|
|
912
|
+
size_t len) {
|
|
913
|
+
return find_key_in(tape_of(owner), owner->doc->string_buf.get(), start, key, len);
|
|
914
|
+
}
|
|
915
|
+
|
|
912
916
|
// Tape index of element k of an array, starting from element j at tape
|
|
913
917
|
// index i, or 0 when the array, which closes at `end`, is shorter.
|
|
914
918
|
size_t walk_to(const uint64_t *tape, size_t i, size_t j, size_t k, size_t end) {
|
|
@@ -1002,6 +1006,234 @@ PyObject *at_pointer(DocumentObject *owner, size_t i, PyObject *arg) {
|
|
|
1002
1006
|
return value_at(owner, i);
|
|
1003
1007
|
}
|
|
1004
1008
|
|
|
1009
|
+
// --- fields= ------------------------------------------------------------------
|
|
1010
|
+
// loads_many(data, fields=...) and map_many(fn, data, fields=...): instead of
|
|
1011
|
+
// each document, the values at some JSON Pointers, found on the tape. Only
|
|
1012
|
+
// these values become Python objects.
|
|
1013
|
+
|
|
1014
|
+
struct FieldStep {
|
|
1015
|
+
std::string key;
|
|
1016
|
+
size_t index; // the token as an array index, or SIZE_MAX
|
|
1017
|
+
PyObject *name; // the token as a str (documents that json decoded)
|
|
1018
|
+
};
|
|
1019
|
+
|
|
1020
|
+
struct Fields {
|
|
1021
|
+
std::vector<std::vector<FieldStep>> paths;
|
|
1022
|
+
std::vector<size_t> shared; // leading steps in common with the previous path
|
|
1023
|
+
size_t depth = 0; // steps of the longest path
|
|
1024
|
+
bool single = false; // fields was one str: the value, not a tuple
|
|
1025
|
+
PyObject *dflt = nullptr; // the value of a missing field
|
|
1026
|
+
|
|
1027
|
+
~Fields() {
|
|
1028
|
+
for (auto &p : paths) {
|
|
1029
|
+
for (auto &s : p) {
|
|
1030
|
+
Py_XDECREF(s.name);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
Py_XDECREF(dflt);
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
// Appends the steps of the JSON Pointer `arg` to `path`.
|
|
1038
|
+
bool compile_pointer(PyObject *arg, std::vector<FieldStep> &path) {
|
|
1039
|
+
Py_ssize_t plen;
|
|
1040
|
+
const char *p = PyUnicode_Check(arg) ? PyUnicode_AsUTF8AndSize(arg, &plen) : nullptr;
|
|
1041
|
+
if (p == nullptr) {
|
|
1042
|
+
if (!PyErr_Occurred()) {
|
|
1043
|
+
PyErr_Format(PyExc_TypeError, "fields must be JSON pointers (str), not %.200s",
|
|
1044
|
+
Py_TYPE(arg)->tp_name);
|
|
1045
|
+
}
|
|
1046
|
+
return false;
|
|
1047
|
+
}
|
|
1048
|
+
if (plen > 0 && p[0] != '/') {
|
|
1049
|
+
PyErr_Format(PyExc_ValueError, "invalid JSON pointer: %R", arg);
|
|
1050
|
+
return false;
|
|
1051
|
+
}
|
|
1052
|
+
const char *end = p + plen;
|
|
1053
|
+
for (const char *cur = p; cur < end;) {
|
|
1054
|
+
cur++; // '/'
|
|
1055
|
+
const char *stop = static_cast<const char *>(memchr(cur, '/', size_t(end - cur)));
|
|
1056
|
+
if (stop == nullptr) {
|
|
1057
|
+
stop = end;
|
|
1058
|
+
}
|
|
1059
|
+
std::string token(cur, size_t(stop - cur));
|
|
1060
|
+
cur = stop;
|
|
1061
|
+
for (size_t t = token.find('~'); t != std::string::npos; t = token.find('~', t + 1)) {
|
|
1062
|
+
if (t + 1 >= token.size() || (token[t + 1] != '0' && token[t + 1] != '1')) {
|
|
1063
|
+
PyErr_Format(PyExc_ValueError, "invalid JSON pointer: %R", arg);
|
|
1064
|
+
return false;
|
|
1065
|
+
}
|
|
1066
|
+
token.replace(t, 2, token[t + 1] == '0' ? "~" : "/");
|
|
1067
|
+
}
|
|
1068
|
+
bool digits = !token.empty() && (token.size() == 1 || token[0] != '0') &&
|
|
1069
|
+
token.size() <= 18;
|
|
1070
|
+
size_t k = 0;
|
|
1071
|
+
for (char ch : token) {
|
|
1072
|
+
digits = digits && ch >= '0' && ch <= '9';
|
|
1073
|
+
k = k * 10 + size_t(ch - '0');
|
|
1074
|
+
}
|
|
1075
|
+
PyObject *name = PyUnicode_FromStringAndSize(token.data(), Py_ssize_t(token.size()));
|
|
1076
|
+
if (name == nullptr) {
|
|
1077
|
+
return false;
|
|
1078
|
+
}
|
|
1079
|
+
path.push_back(FieldStep{std::move(token), digits ? k : SIZE_MAX, name});
|
|
1080
|
+
}
|
|
1081
|
+
return true;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// The fields of `spec` (a JSON Pointer, or a list or tuple of them), or
|
|
1085
|
+
// nullptr with an exception.
|
|
1086
|
+
Fields *compile_fields(PyObject *spec, PyObject *dflt) {
|
|
1087
|
+
Fields *f = new (std::nothrow) Fields();
|
|
1088
|
+
if (f == nullptr) {
|
|
1089
|
+
PyErr_NoMemory();
|
|
1090
|
+
return nullptr;
|
|
1091
|
+
}
|
|
1092
|
+
f->dflt = Py_NewRef(dflt != nullptr ? dflt : Py_None);
|
|
1093
|
+
f->single = PyUnicode_Check(spec);
|
|
1094
|
+
PyObject *seq = f->single ? PyTuple_Pack(1, spec)
|
|
1095
|
+
: PyList_Check(spec) || PyTuple_Check(spec)
|
|
1096
|
+
? PySequence_Tuple(spec)
|
|
1097
|
+
: nullptr;
|
|
1098
|
+
if (seq == nullptr) {
|
|
1099
|
+
if (!PyErr_Occurred()) {
|
|
1100
|
+
PyErr_Format(PyExc_TypeError,
|
|
1101
|
+
"fields must be a JSON pointer (str) or a list or tuple of them, not %.200s",
|
|
1102
|
+
Py_TYPE(spec)->tp_name);
|
|
1103
|
+
}
|
|
1104
|
+
delete f;
|
|
1105
|
+
return nullptr;
|
|
1106
|
+
}
|
|
1107
|
+
for (Py_ssize_t n = 0; n < PyTuple_GET_SIZE(seq); n++) {
|
|
1108
|
+
f->paths.emplace_back();
|
|
1109
|
+
if (!compile_pointer(PyTuple_GET_ITEM(seq, n), f->paths.back())) {
|
|
1110
|
+
Py_DECREF(seq);
|
|
1111
|
+
delete f;
|
|
1112
|
+
return nullptr;
|
|
1113
|
+
}
|
|
1114
|
+
const std::vector<FieldStep> &path = f->paths.back();
|
|
1115
|
+
size_t common = 0;
|
|
1116
|
+
if (n > 0) {
|
|
1117
|
+
const std::vector<FieldStep> &prev = f->paths[size_t(n) - 1];
|
|
1118
|
+
while (common < path.size() && common < prev.size() &&
|
|
1119
|
+
path[common].key == prev[common].key) {
|
|
1120
|
+
common++;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
f->shared.push_back(common);
|
|
1124
|
+
f->depth = std::max(f->depth, path.size());
|
|
1125
|
+
}
|
|
1126
|
+
Py_DECREF(seq);
|
|
1127
|
+
return f;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// The fields of the document on the tape (root at index 1).
|
|
1131
|
+
PyObject *extract_fields(const document &doc, const Fields &f) {
|
|
1132
|
+
const uint64_t *tape = doc.tape.get();
|
|
1133
|
+
const uint8_t *strings = doc.string_buf.get();
|
|
1134
|
+
Builder b(doc);
|
|
1135
|
+
size_t n = f.paths.size();
|
|
1136
|
+
PyObject *tuple = f.single ? nullptr : PyTuple_New(Py_ssize_t(n));
|
|
1137
|
+
if (!f.single && tuple == nullptr) {
|
|
1138
|
+
return nullptr;
|
|
1139
|
+
}
|
|
1140
|
+
// pos[d]: tape index reached after d steps of the previous path, which
|
|
1141
|
+
// reached `reached` steps; paths that share a prefix walk it once.
|
|
1142
|
+
size_t stack[16];
|
|
1143
|
+
std::vector<size_t> heap;
|
|
1144
|
+
size_t *pos = stack;
|
|
1145
|
+
if (f.depth + 1 > 16) {
|
|
1146
|
+
heap.resize(f.depth + 1);
|
|
1147
|
+
pos = heap.data();
|
|
1148
|
+
}
|
|
1149
|
+
pos[0] = 1;
|
|
1150
|
+
size_t reached = 0;
|
|
1151
|
+
for (size_t p = 0; p < n; p++) {
|
|
1152
|
+
const std::vector<FieldStep> &path = f.paths[p];
|
|
1153
|
+
size_t d = std::min(f.shared[p], reached);
|
|
1154
|
+
size_t i = pos[d];
|
|
1155
|
+
for (; d < path.size(); d++) {
|
|
1156
|
+
uint8_t type = uint8_t(tape[i] >> 56);
|
|
1157
|
+
const FieldStep &s = path[d];
|
|
1158
|
+
if (type == '{') {
|
|
1159
|
+
i = find_key_in(tape, strings, i, s.key.data(), s.key.size());
|
|
1160
|
+
} else if (type == '[' && s.index != SIZE_MAX) {
|
|
1161
|
+
i = walk_to(tape, i + 1, 0, s.index, close_of(tape, i));
|
|
1162
|
+
} else {
|
|
1163
|
+
i = 0;
|
|
1164
|
+
}
|
|
1165
|
+
if (i == 0) {
|
|
1166
|
+
break;
|
|
1167
|
+
}
|
|
1168
|
+
pos[d + 1] = i;
|
|
1169
|
+
}
|
|
1170
|
+
reached = d;
|
|
1171
|
+
PyObject *v;
|
|
1172
|
+
if (d == path.size()) {
|
|
1173
|
+
v = b.build(i);
|
|
1174
|
+
if (v == nullptr) {
|
|
1175
|
+
Py_XDECREF(tuple);
|
|
1176
|
+
return nullptr;
|
|
1177
|
+
}
|
|
1178
|
+
} else {
|
|
1179
|
+
v = Py_NewRef(f.dflt);
|
|
1180
|
+
}
|
|
1181
|
+
if (f.single) {
|
|
1182
|
+
return v;
|
|
1183
|
+
}
|
|
1184
|
+
PyTuple_SET_ITEM(tuple, Py_ssize_t(p), v);
|
|
1185
|
+
}
|
|
1186
|
+
return tuple;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
PyObject *extract_fields_gc_paused(const document &doc, const Fields &f) {
|
|
1190
|
+
#ifndef Py_GIL_DISABLED
|
|
1191
|
+
int gc_was_enabled = PyGC_Disable();
|
|
1192
|
+
#endif
|
|
1193
|
+
PyObject *res = extract_fields(doc, f);
|
|
1194
|
+
#ifndef Py_GIL_DISABLED
|
|
1195
|
+
if (gc_was_enabled) {
|
|
1196
|
+
PyGC_Enable();
|
|
1197
|
+
}
|
|
1198
|
+
#endif
|
|
1199
|
+
return res;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// The fields of a document that json decoded into Python objects.
|
|
1203
|
+
PyObject *extract_fields_py(PyObject *value, const Fields &f) {
|
|
1204
|
+
size_t n = f.paths.size();
|
|
1205
|
+
PyObject *tuple = f.single ? nullptr : PyTuple_New(Py_ssize_t(n));
|
|
1206
|
+
if (!f.single && tuple == nullptr) {
|
|
1207
|
+
return nullptr;
|
|
1208
|
+
}
|
|
1209
|
+
for (size_t p = 0; p < n; p++) {
|
|
1210
|
+
PyObject *v = value; // borrowed
|
|
1211
|
+
for (const FieldStep &s : f.paths[p]) {
|
|
1212
|
+
if (PyDict_Check(v)) {
|
|
1213
|
+
v = PyDict_GetItemWithError(v, s.name);
|
|
1214
|
+
if (v == nullptr && PyErr_Occurred()) {
|
|
1215
|
+
Py_XDECREF(tuple);
|
|
1216
|
+
return nullptr;
|
|
1217
|
+
}
|
|
1218
|
+
} else if (PyList_Check(v) && s.index != SIZE_MAX &&
|
|
1219
|
+
s.index < size_t(PyList_GET_SIZE(v))) {
|
|
1220
|
+
v = PyList_GET_ITEM(v, Py_ssize_t(s.index));
|
|
1221
|
+
} else {
|
|
1222
|
+
v = nullptr;
|
|
1223
|
+
}
|
|
1224
|
+
if (v == nullptr) {
|
|
1225
|
+
break;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
v = Py_NewRef(v != nullptr ? v : f.dflt);
|
|
1229
|
+
if (f.single) {
|
|
1230
|
+
return v;
|
|
1231
|
+
}
|
|
1232
|
+
PyTuple_SET_ITEM(tuple, Py_ssize_t(p), v);
|
|
1233
|
+
}
|
|
1234
|
+
return tuple;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1005
1237
|
// --- Document ---------------------------------------------------------------
|
|
1006
1238
|
|
|
1007
1239
|
void free_copy(document *d);
|
|
@@ -3102,6 +3334,8 @@ struct StreamObject {
|
|
|
3102
3334
|
bool no_fallback; // a chunk of a parallel run: do not call json (see run_chunk)
|
|
3103
3335
|
bool need_fallback; // set instead of calling json, with no_fallback
|
|
3104
3336
|
bool borrowed_parser; // p is this thread's chunk parser (not deleted)
|
|
3337
|
+
Fields *fields; // fields=: the values at these pointers, not the documents
|
|
3338
|
+
bool owns_fields; // false in the chunks of a parallel run
|
|
3105
3339
|
parser *p;
|
|
3106
3340
|
document_stream *stream;
|
|
3107
3341
|
document_stream::iterator it;
|
|
@@ -3266,6 +3500,9 @@ void free_copy(document *d) {
|
|
|
3266
3500
|
}
|
|
3267
3501
|
|
|
3268
3502
|
PyObject *stream_value(StreamObject *s) {
|
|
3503
|
+
if (s->fields != nullptr) {
|
|
3504
|
+
return extract_fields_gc_paused(s->p->doc, *s->fields);
|
|
3505
|
+
}
|
|
3269
3506
|
if (!s->lazy) {
|
|
3270
3507
|
return build_with_gc_paused(s->p->doc, 1);
|
|
3271
3508
|
}
|
|
@@ -3313,6 +3550,11 @@ PyObject *stream_fallback(StreamObject *s) {
|
|
|
3313
3550
|
return nullptr;
|
|
3314
3551
|
}
|
|
3315
3552
|
s->active = false; // resume with simdjson after this document
|
|
3553
|
+
if (s->fields != nullptr) {
|
|
3554
|
+
PyObject *v = extract_fields_py(value, *s->fields);
|
|
3555
|
+
Py_DECREF(value);
|
|
3556
|
+
return v;
|
|
3557
|
+
}
|
|
3316
3558
|
// As with parse(), a document that only json accepts is returned as plain
|
|
3317
3559
|
// Python objects.
|
|
3318
3560
|
return value;
|
|
@@ -3387,6 +3629,9 @@ void stream_dealloc(PyObject *self) {
|
|
|
3387
3629
|
} else {
|
|
3388
3630
|
delete s->p;
|
|
3389
3631
|
}
|
|
3632
|
+
if (s->owns_fields) {
|
|
3633
|
+
delete s->fields;
|
|
3634
|
+
}
|
|
3390
3635
|
delete s->copy;
|
|
3391
3636
|
Py_XDECREF(s->source);
|
|
3392
3637
|
Py_XDECREF(s->text);
|
|
@@ -3474,6 +3719,8 @@ PyObject *make_stream(PyObject *args, PyObject *kwargs, bool lazy) {
|
|
|
3474
3719
|
s->base = 0;
|
|
3475
3720
|
s->last_end = 0;
|
|
3476
3721
|
s->no_fallback = s->need_fallback = s->borrowed_parser = false;
|
|
3722
|
+
s->fields = nullptr;
|
|
3723
|
+
s->owns_fields = false;
|
|
3477
3724
|
PyObject *self = reinterpret_cast<PyObject *>(s);
|
|
3478
3725
|
if (must_copy) {
|
|
3479
3726
|
PyObject *b = PyBytes_FromObject(data);
|
|
@@ -3574,6 +3821,8 @@ StreamObject *stream_over(StreamObject *whole, size_t from, size_t to, bool chun
|
|
|
3574
3821
|
s->open = whole->open;
|
|
3575
3822
|
s->close = whole->close;
|
|
3576
3823
|
s->no_fallback = s->need_fallback = s->borrowed_parser = false;
|
|
3824
|
+
s->fields = whole->fields; // whole outlives its chunks
|
|
3825
|
+
s->owns_fields = false;
|
|
3577
3826
|
if (chunk && !g_thread_parser.chunk_parser_busy) {
|
|
3578
3827
|
if (g_thread_parser.chunk_parser == nullptr) {
|
|
3579
3828
|
register_thread_cleanup();
|
|
@@ -4011,21 +4260,67 @@ PyObject *take_threads(PyObject *kwargs, Py_ssize_t *threads, bool *given) {
|
|
|
4011
4260
|
return rest;
|
|
4012
4261
|
}
|
|
4013
4262
|
|
|
4263
|
+
// Removes the keyword `name` from the dict kwargs (may be nullptr); returns
|
|
4264
|
+
// its value (new reference), or nullptr when absent or on error.
|
|
4265
|
+
PyObject *pop_keyword(PyObject *kwargs, const char *name) {
|
|
4266
|
+
if (kwargs == nullptr) {
|
|
4267
|
+
return nullptr;
|
|
4268
|
+
}
|
|
4269
|
+
PyObject *v = PyDict_GetItemString(kwargs, name); // borrowed
|
|
4270
|
+
if (v == nullptr) {
|
|
4271
|
+
return nullptr;
|
|
4272
|
+
}
|
|
4273
|
+
Py_INCREF(v);
|
|
4274
|
+
if (PyDict_DelItemString(kwargs, name) < 0) {
|
|
4275
|
+
Py_DECREF(v);
|
|
4276
|
+
return nullptr;
|
|
4277
|
+
}
|
|
4278
|
+
return v;
|
|
4279
|
+
}
|
|
4280
|
+
|
|
4014
4281
|
// The stream for loads_many/parse_many/map_many; with threads, the list of
|
|
4015
|
-
// all results instead.
|
|
4282
|
+
// all results instead. With fields (not for parse_many), the values at the
|
|
4283
|
+
// fields' pointers come instead of the documents.
|
|
4016
4284
|
PyObject *stream_or_list(PyObject *args, PyObject *kwargs, bool lazy, PyObject *fn,
|
|
4017
|
-
bool force_list) {
|
|
4285
|
+
bool force_list, bool allow_fields) {
|
|
4018
4286
|
Py_ssize_t threads;
|
|
4019
4287
|
bool given;
|
|
4020
4288
|
PyObject *rest = take_threads(kwargs, &threads, &given);
|
|
4021
4289
|
if (rest == nullptr && PyErr_Occurred()) {
|
|
4022
4290
|
return nullptr;
|
|
4023
4291
|
}
|
|
4292
|
+
Fields *fields = nullptr;
|
|
4293
|
+
if (allow_fields) {
|
|
4294
|
+
PyObject *spec = pop_keyword(rest, "fields");
|
|
4295
|
+
PyObject *dflt = PyErr_Occurred() ? nullptr : pop_keyword(rest, "default");
|
|
4296
|
+
if (PyErr_Occurred()) {
|
|
4297
|
+
// reported below
|
|
4298
|
+
} else if (spec != nullptr && spec != Py_None) {
|
|
4299
|
+
if (lazy) {
|
|
4300
|
+
PyErr_SetString(PyExc_ValueError, "fields cannot be combined with lazy=True");
|
|
4301
|
+
} else {
|
|
4302
|
+
fields = compile_fields(spec, dflt);
|
|
4303
|
+
}
|
|
4304
|
+
} else if (dflt != nullptr) {
|
|
4305
|
+
PyErr_SetString(PyExc_TypeError, "default requires fields");
|
|
4306
|
+
}
|
|
4307
|
+
Py_XDECREF(spec);
|
|
4308
|
+
Py_XDECREF(dflt);
|
|
4309
|
+
if (PyErr_Occurred()) {
|
|
4310
|
+
Py_XDECREF(rest);
|
|
4311
|
+
return nullptr;
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4024
4314
|
PyObject *self = make_stream(args, rest, lazy);
|
|
4025
4315
|
Py_XDECREF(rest);
|
|
4026
4316
|
if (self == nullptr) {
|
|
4317
|
+
delete fields;
|
|
4027
4318
|
return nullptr;
|
|
4028
4319
|
}
|
|
4320
|
+
if (fields != nullptr) {
|
|
4321
|
+
reinterpret_cast<StreamObject *>(self)->fields = fields;
|
|
4322
|
+
reinterpret_cast<StreamObject *>(self)->owns_fields = true;
|
|
4323
|
+
}
|
|
4029
4324
|
if (!given && !force_list) {
|
|
4030
4325
|
return self;
|
|
4031
4326
|
}
|
|
@@ -4045,16 +4340,16 @@ PyObject *stream_or_list(PyObject *args, PyObject *kwargs, bool lazy, PyObject *
|
|
|
4045
4340
|
}
|
|
4046
4341
|
|
|
4047
4342
|
PyObject *loads_many(PyObject *, PyObject *args, PyObject *kwargs) {
|
|
4048
|
-
return stream_or_list(args, kwargs, false, nullptr, false);
|
|
4343
|
+
return stream_or_list(args, kwargs, false, nullptr, false, true);
|
|
4049
4344
|
}
|
|
4050
4345
|
|
|
4051
4346
|
PyObject *parse_many(PyObject *, PyObject *args, PyObject *kwargs) {
|
|
4052
|
-
return stream_or_list(args, kwargs, true, nullptr, false);
|
|
4347
|
+
return stream_or_list(args, kwargs, true, nullptr, false, false);
|
|
4053
4348
|
}
|
|
4054
4349
|
|
|
4055
|
-
// map_many(fn, data, *, format, batch_size, threads, lazy=False
|
|
4056
|
-
// doc in loads_many(data)] (parse_many with
|
|
4057
|
-
// threads.
|
|
4350
|
+
// map_many(fn, data, *, format, batch_size, threads, lazy=False, fields,
|
|
4351
|
+
// default): [fn(doc) for doc in loads_many(data)] (parse_many with
|
|
4352
|
+
// lazy=True), with fn called in the threads.
|
|
4058
4353
|
PyObject *map_many(PyObject *, PyObject *args, PyObject *kwargs) {
|
|
4059
4354
|
if (PyTuple_GET_SIZE(args) < 1) {
|
|
4060
4355
|
PyErr_SetString(PyExc_TypeError, "map_many() missing required argument 'fn'");
|
|
@@ -4086,7 +4381,7 @@ PyObject *map_many(PyObject *, PyObject *args, PyObject *kwargs) {
|
|
|
4086
4381
|
Py_XDECREF(rest);
|
|
4087
4382
|
return nullptr;
|
|
4088
4383
|
}
|
|
4089
|
-
PyObject *r = stream_or_list(tail, rest, lazy, fn, true);
|
|
4384
|
+
PyObject *r = stream_or_list(tail, rest, lazy, fn, true, true);
|
|
4090
4385
|
Py_DECREF(tail);
|
|
4091
4386
|
Py_XDECREF(rest);
|
|
4092
4387
|
return r;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastsimdjson
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Fast JSON parsing for Python, built on simdjson
|
|
5
5
|
Author-email: Daniel Lemire <daniel@lemire.me>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -25,6 +25,7 @@ Provides-Extra: test
|
|
|
25
25
|
Requires-Dist: pytest; extra == "test"
|
|
26
26
|
Provides-Extra: bench
|
|
27
27
|
Requires-Dist: orjson; extra == "bench"
|
|
28
|
+
Requires-Dist: jiter; extra == "bench"
|
|
28
29
|
Requires-Dist: msgspec; extra == "bench"
|
|
29
30
|
Requires-Dist: pysimdjson; extra == "bench"
|
|
30
31
|
Requires-Dist: cysimdjson; extra == "bench"
|
|
@@ -38,7 +39,8 @@ replacement for the `json` module's `loads`, `load`, `dumps` and `dump`, and
|
|
|
38
39
|
it can be over 3 times faster than the standard `json.loads` and
|
|
39
40
|
`json.dumps`. When you only need part of a document, its lazy `parse`
|
|
40
41
|
function is faster still. It also reads streams of documents (NDJSON, JSON
|
|
41
|
-
Lines)
|
|
42
|
+
Lines), on many threads with free-threaded Python, and can extract only the
|
|
43
|
+
fields you need from each document. It writes JSON in two ways: `dumps` returns the same `str` as
|
|
42
44
|
`json.dumps`, and `dumpb` returns the same `bytes` as `orjson.dumps`, as fast
|
|
43
45
|
as orjson.
|
|
44
46
|
|
|
@@ -233,6 +235,37 @@ document, and the Python objects of all of them, to the calling thread.
|
|
|
233
235
|
With the GIL (the default build of Python), the calling thread does all the
|
|
234
236
|
work: `threads` has no effect on speed.
|
|
235
237
|
|
|
238
|
+
### Only some fields: `fields=`
|
|
239
|
+
|
|
240
|
+
When you need a few values from each document, `fields` extracts them
|
|
241
|
+
without building the documents: only the values found become Python
|
|
242
|
+
objects.
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
events = fastsimdjson.loads_many(data, fields=["/type", "/payload/action"])
|
|
246
|
+
# (type, action) for each document
|
|
247
|
+
|
|
248
|
+
def count(t):
|
|
249
|
+
kind, action, language = t
|
|
250
|
+
...
|
|
251
|
+
fastsimdjson.map_many(count, data, threads=64,
|
|
252
|
+
fields=["/type", "/payload/action",
|
|
253
|
+
"/payload/pull_request/base/repo/language"])
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
`loads_many` and `map_many` accept `fields`: a JSON Pointer (RFC 6901), or a
|
|
257
|
+
list or tuple of them. Each document then becomes the value at the pointer,
|
|
258
|
+
or a tuple of the values at the pointers, in order. A value that is an
|
|
259
|
+
object or an array is built in full. A pointer that leads nowhere (a
|
|
260
|
+
missing key, an index past the end, or a step into a scalar) gives
|
|
261
|
+
`default`, `None` unless you pass another value; a JSON `null` gives `None`.
|
|
262
|
+
As with `at_pointer`, a key that appears twice in an object refers to its
|
|
263
|
+
first occurrence. `fields` cannot be combined with `lazy=True`.
|
|
264
|
+
|
|
265
|
+
On 7.6 GB of GitHub events, `map_many` with three fields ran at 2.8 GB/s on
|
|
266
|
+
one core of an Intel Xeon Gold 6548N, against 2.2 GB/s with lazy views
|
|
267
|
+
(`lazy=True`). With 64 threads, it ran at 78 GB/s (lazy views: 61 GB/s).
|
|
268
|
+
|
|
236
269
|
### `release`
|
|
237
270
|
|
|
238
271
|
`release()` frees the simdjson parser and the string caches kept by the
|
|
@@ -307,8 +340,8 @@ The suite builds an ~80 MB document and a list of 16,777,221 integers, so
|
|
|
307
340
|
give it some RAM.
|
|
308
341
|
|
|
309
342
|
The benchmark scripts need simdjson-data and the `bench` extra (orjson,
|
|
310
|
-
msgspec, pysimdjson, cysimdjson). `bench.py` times `loads` against
|
|
311
|
-
`json.loads
|
|
343
|
+
jiter, msgspec, pysimdjson, cysimdjson). `bench.py` times `loads` against
|
|
344
|
+
`json.loads`, orjson and jiter, `bench_lazy.py` times `parse` against pysimdjson and
|
|
312
345
|
cysimdjson, `bench_dumps.py` times `dumps` against `json.dumps` and `dumpb`
|
|
313
346
|
against orjson and msgspec, and `bench_many.py` times `loads_many`.
|
|
314
347
|
|
|
@@ -347,7 +380,7 @@ geometric mean over the 22 files (higher is better).
|
|
|
347
380
|
|
|
348
381
|
<picture>
|
|
349
382
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_loads_dark.png">
|
|
350
|
-
<img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_loads.png" width="80%" alt="Parsing JSON in Python: fastsimdjson loads 0.78 GB/s, orjson 0.60, msgspec 0.53, pysimdjson 0.44, cysimdjson 0.43, ujson 0.37, python-rapidjson 0.24, simplejson 0.23, json 0.22">
|
|
383
|
+
<img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_loads.png" width="80%" alt="Parsing JSON in Python: fastsimdjson loads 0.78 GB/s, orjson 0.60, msgspec 0.53, jiter 0.48, pysimdjson 0.44, cysimdjson 0.43, ujson 0.37, python-rapidjson 0.24, simplejson 0.23, json 0.22">
|
|
351
384
|
</picture>
|
|
352
385
|
|
|
353
386
|
| parser | GB/s | vs `json.loads` |
|
|
@@ -358,6 +391,7 @@ geometric mean over the 22 files (higher is better).
|
|
|
358
391
|
| ujson 6.0.0 | 0.37 | 1.66× |
|
|
359
392
|
| cysimdjson 26.27 | 0.43 | 1.96× |
|
|
360
393
|
| pysimdjson 7.0.2 | 0.44 | 1.98× |
|
|
394
|
+
| jiter 0.17.0 | 0.48 | 2.21× |
|
|
361
395
|
| msgspec 0.22.0 | 0.53 | 2.42× |
|
|
362
396
|
| orjson 3.12.0 | 0.60 | 2.74× |
|
|
363
397
|
| **fastsimdjson `loads`** | **0.78** | **3.53×** |
|
|
@@ -371,7 +405,9 @@ gain comes from pausing the garbage collector while the objects are built.
|
|
|
371
405
|
Timing each call on its own, fastsimdjson's lead over orjson is 1.33× with
|
|
372
406
|
the collector enabled and 1.22× with it disabled (geometric means); without
|
|
373
407
|
the collector, `canada.json`, `mesh.json` and `numbers.json` are ties. yyjson 4.0.6 is left out: it
|
|
374
|
-
returns wrong strings for non-ASCII text.
|
|
408
|
+
returns wrong strings for non-ASCII text. jiter (the parser of pydantic)
|
|
409
|
+
caches strings by default; turning the cache off (`cache_mode=False`) makes it
|
|
410
|
+
slower here (0.44 GB/s).
|
|
375
411
|
|
|
376
412
|
Parsing is no longer the bottleneck. simdjson alone parses these files at
|
|
377
413
|
3.1 GB/s. It accounts for about a third of the time of `loads`; the rest goes
|
|
@@ -381,3 +381,104 @@ def test_map_many_lazy_views_kept():
|
|
|
381
381
|
with pytest.raises(ValueError) as info:
|
|
382
382
|
fastsimdjson.map_many(boom, data, threads=threads, lazy=True)
|
|
383
383
|
assert to_py(info.value.args[0]) == expected[20000]
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# fields=: the values at JSON Pointers instead of the documents.
|
|
387
|
+
|
|
388
|
+
_MISSING = object()
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _pointer_get(doc, pointer, default=None):
|
|
392
|
+
if pointer == "":
|
|
393
|
+
return doc
|
|
394
|
+
for token in pointer[1:].split("/"):
|
|
395
|
+
token = token.replace("~1", "/").replace("~0", "~")
|
|
396
|
+
if isinstance(doc, dict) and token in doc:
|
|
397
|
+
doc = doc[token]
|
|
398
|
+
elif (isinstance(doc, list) and token.isdigit() and (token == "0" or token[0] != "0")
|
|
399
|
+
and int(token) < len(doc)):
|
|
400
|
+
doc = doc[int(token)]
|
|
401
|
+
else:
|
|
402
|
+
return default
|
|
403
|
+
return doc
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _pointers(doc, prefix=""):
|
|
407
|
+
yield prefix
|
|
408
|
+
if isinstance(doc, dict):
|
|
409
|
+
for k, v in doc.items():
|
|
410
|
+
yield from _pointers(v, prefix + "/" + k.replace("~", "~0").replace("/", "~1"))
|
|
411
|
+
elif isinstance(doc, list):
|
|
412
|
+
for i, v in enumerate(doc):
|
|
413
|
+
yield from _pointers(v, f"{prefix}/{i}")
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
@pytest.mark.parametrize("seed", range(30))
|
|
417
|
+
def test_fields_random(seed):
|
|
418
|
+
rnd = random.Random(seed)
|
|
419
|
+
docs = [random_value(rnd) for _ in range(40)]
|
|
420
|
+
docs += [{"a/b": {"~": [1, {"k1": 2}]}, "": 3, "07": 4}, ["x", ["y"]]]
|
|
421
|
+
candidates = sorted({p for d in docs for p in _pointers(d)})
|
|
422
|
+
candidates += ["/nope", "/k1/nope", "/0/0/0", "/00", "/-", "/1x", "/a~1b/~0/1/k1", "/", "/07"]
|
|
423
|
+
text = ndjson(docs).encode()
|
|
424
|
+
for _ in range(10):
|
|
425
|
+
pointers = rnd.sample(candidates, min(len(candidates), rnd.randrange(1, 8)))
|
|
426
|
+
if rnd.random() < 0.5:
|
|
427
|
+
pointers.sort() # shared prefixes, one after the other
|
|
428
|
+
got = list(fastsimdjson.loads_many(text, fields=pointers))
|
|
429
|
+
assert len(got) == len(docs)
|
|
430
|
+
for g, d in zip(got, docs):
|
|
431
|
+
assert isinstance(g, tuple)
|
|
432
|
+
assert all(same(a, _pointer_get(d, p)) for a, p in zip(g, pointers))
|
|
433
|
+
one = pointers[0]
|
|
434
|
+
got = list(fastsimdjson.loads_many(text, fields=one, default=_MISSING))
|
|
435
|
+
assert all(same(a, _pointer_get(d, one, _MISSING)) for a, d in zip(got, docs))
|
|
436
|
+
assert fastsimdjson.map_many(lambda t: t, text, fields=tuple(pointers)) == \
|
|
437
|
+
list(fastsimdjson.loads_many(text, fields=pointers))
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def test_fields_api():
|
|
441
|
+
data = b'{"a": 1, "b": {"c": [10, 20, {"d": "x"}]}}\n{"a": null}\n[5]\n7\n'
|
|
442
|
+
assert list(fastsimdjson.loads_many(data, fields=["/a", "/b/c/2/d", "/b/c/2", "/0"])) == [
|
|
443
|
+
(1, "x", {"d": "x"}, None), (None, None, None, None), (None, None, None, 5),
|
|
444
|
+
(None, None, None, None)]
|
|
445
|
+
# A missing field gets the default; null is a value.
|
|
446
|
+
assert list(fastsimdjson.loads_many(data, fields="/a", default=0)) == [1, None, 0, 0]
|
|
447
|
+
assert list(fastsimdjson.loads_many(data, fields="")) == list(fastsimdjson.loads_many(data))
|
|
448
|
+
assert list(fastsimdjson.loads_many(data, fields=[])) == [(), (), (), ()]
|
|
449
|
+
assert list(fastsimdjson.loads_many(data, fields=None)) == list(fastsimdjson.loads_many(data))
|
|
450
|
+
# Duplicate keys: the first one, as for views (at_pointer).
|
|
451
|
+
assert list(fastsimdjson.loads_many(b'{"a": 1, "a": 2}', fields="/a")) == [1]
|
|
452
|
+
# Documents that only json accepts.
|
|
453
|
+
assert list(fastsimdjson.loads_many(b'{"a": 1e400, "b": [1]}\n{"a": 2}',
|
|
454
|
+
fields=["/a", "/b/0"])) == [(float("inf"), 1), (2, None)]
|
|
455
|
+
for bad in ["a", "/~2", "/~"]:
|
|
456
|
+
with pytest.raises(ValueError):
|
|
457
|
+
fastsimdjson.loads_many(data, fields=bad)
|
|
458
|
+
for bad in [1, ["/a", 2], {"/a"}]:
|
|
459
|
+
with pytest.raises(TypeError):
|
|
460
|
+
fastsimdjson.loads_many(data, fields=bad)
|
|
461
|
+
with pytest.raises(ValueError):
|
|
462
|
+
fastsimdjson.map_many(len, data, fields="/a", lazy=True)
|
|
463
|
+
with pytest.raises(TypeError):
|
|
464
|
+
fastsimdjson.loads_many(data, default=1)
|
|
465
|
+
with pytest.raises(TypeError):
|
|
466
|
+
fastsimdjson.parse_many(data, fields="/a")
|
|
467
|
+
with pytest.raises(fastsimdjson.JSONDecodeError):
|
|
468
|
+
list(fastsimdjson.loads_many(b'{"a": 1}\n{"a": ', fields="/a"))
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
@pytest.mark.parametrize("fmt", ["whitespace", "lines", "json_seq"])
|
|
472
|
+
@pytest.mark.parametrize("seed", range(6))
|
|
473
|
+
def test_fields_threads(fmt, seed):
|
|
474
|
+
data = _threaded_input(seed, fmt)
|
|
475
|
+
pointers = ["/i", "/s", "/big", "/0", "/pretty/1", "/f"]
|
|
476
|
+
expected = _outcome(lambda: list(fastsimdjson.loads_many(data, format=fmt, fields=pointers)))
|
|
477
|
+
if expected[0] == "ok":
|
|
478
|
+
docs = list(fastsimdjson.loads_many(data, format=fmt))
|
|
479
|
+
assert expected[1] == [tuple(_pointer_get(d, p) for p in pointers) for d in docs]
|
|
480
|
+
for threads in (None, 1, 4):
|
|
481
|
+
assert _outcome(lambda: fastsimdjson.loads_many(data, format=fmt, fields=pointers,
|
|
482
|
+
threads=threads)) == expected
|
|
483
|
+
assert _outcome(lambda: fastsimdjson.map_many(lambda t: t, data, format=fmt,
|
|
484
|
+
fields=pointers, threads=threads)) == expected
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|