mirror of
https://github.com/nlohmann/json.git
synced 2026-11-07 04:59:21 +00:00
Parse JSON in one block of memory with a fused pointer-based parser
The parser and the lexer exchange one token at a time, and the lexer keeps its state (the current character, a pending unget, where the token starts) in members, which every character pays for: json::accept() of twitter.json took 2.8x as long as json_document::accept(). detail::contiguous_parser reads text in one block of memory through a local pointer, with the grammar, the whitespace and the tokens in one loop (after yyjson's reader), and drives a SAX handler: parse()'s DOM handler, accept()'s handler, or the user's handler of sax_parse(). It is used with the default options (no callback, no comments, no trailing commas; sax_parse() with strict = true). Errors are reported by the library parser, so the exception, its message and its position stay the same. contiguous_parser stops at the token that the library parser would reject, before reporting it, so the events up to the error are the same; then: - parse() runs the library parser with a handler that only reports the error (json_sax_error_reporter), with the lexer of parse(), which is instantiated anyway; - sax_parse() runs it with a handler that forwards only parse_error() to the user's handler (json_sax_error_forwarder), so no event arrives twice; - accept() returns the verdict. A handler that returns false stops the parse as before. An error at the end of a large input costs parse() about 1.2x of what it cost before. On an Apple M1 Max: accept() 1.6-2.0x as fast, parse() 10-16% faster, ordered_json 11-16%, sax_parse() with a user's handler 1.36-1.46x, small messages 12-22% (parse()) and 2.1-2.4x (accept()). On x86-64 (GCC 15, Clang 21): accept() 1.6-1.9x, parse() 5-17%, ordered_json 12-19%, sax_parse() 1.32-1.57x. A translation unit that parses a string compiles 3% (-O0) and 9% (-O2) slower. unit-contiguous_parser.cpp compares the contiguous path with the byte path (std::list iterators) on corner cases, generated documents, their truncations and mutations, deep nesting, and narrow number types: the same values with the same number types and float bits, exception messages, accept() verdicts and sax_parse() events. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
9 files changed
+2654
-16
No files matched your search
@@ -48,8 +48,8 @@ The default value is `0`: streams are scanned in bulk.
|
||||
!!! note "Performance"
|
||||
|
||||
Parsing from a `std::ifstream` or `std::istringstream` is typically 1.5 to 2 times as fast as before, and reading
|
||||
many small values from one stream with `operator>>` 1.4 to 4 times as fast. Parsing from a stream is then within
|
||||
about 10 to 20 % of first reading the whole file into a `std::string` and parsing that.
|
||||
many small values from one stream with `operator>>` 1.4 to 4 times as fast. Parsing from a stream then takes about
|
||||
1.15 to 1.45 times as long as first reading the whole file into a `std::string` and parsing that.
|
||||
|
||||
A stream buffer without a get area (one that only overrides `underflow()` and `uflow()` and hands out every
|
||||
character from there) offers nothing to scan in bulk, so it is read one character at a time as before.
|
||||
|
||||
@@ -32,11 +32,11 @@ json read_json_file(const std::string& path)
|
||||
column 1 instead of saying that the file is missing. A relative path is resolved against the working directory of the
|
||||
running program, which is often not the directory of the source file (see the
|
||||
[FAQ](../home/faq.md#parsing-a-file-fails-with-unexpected-end-of-input)).
|
||||
- **Don't read the file into a string first.** Parsing from a `#!cpp std::ifstream` is only about 10 to 20 % slower
|
||||
than reading the whole file into a string with one `read()` and parsing that, and it needs no memory for the copy (see
|
||||
[input sources and performance](parsing/index.md#input-sources-and-performance)). The common way to read a file with
|
||||
`#!cpp std::istreambuf_iterator<char>` is so slow that it takes about twice as long as parsing from the stream. Read
|
||||
the file into a string only if you need the text itself, for example for an error message. This advice is for
|
||||
- **Read the file into a string only if the memory is no concern.** Parsing from a `#!cpp std::ifstream` takes about
|
||||
1.15 to 1.45 times as long as reading the whole file into a string with one `read()` and parsing that, but it needs no
|
||||
memory for the copy (see [input sources and performance](parsing/index.md#input-sources-and-performance)). The common
|
||||
way to read a file with `#!cpp std::istreambuf_iterator<char>` is so slow that it takes about twice as long as parsing
|
||||
from the stream. This advice is for
|
||||
`#!cpp json::parse`. [`json_document`](json_view.md) reads a stream into a buffer that it owns anyway, so there is no
|
||||
copy to save: reading the file into a `#!cpp std::string` yourself and passing that as an lvalue, which the document
|
||||
borrows, was about 6 to 12 % faster than parsing from the stream, with the same single buffer.
|
||||
|
||||
@@ -80,8 +80,13 @@ them faster than others:
|
||||
| `#!cpp FILE*` | one character at a time, with the file locked once for the whole parse |
|
||||
| other iterator ranges (e.g., `#!cpp std::list<char>`, `#!cpp std::istreambuf_iterator<char>`), wide strings | one character at a time |
|
||||
|
||||
Parsing a file from a `#!cpp std::ifstream` is therefore only about 10 to 20 % slower than reading the whole file into a
|
||||
`#!cpp std::string` first and parsing that, and it needs no memory for a copy of the file. The stream is never read
|
||||
Input in one block of memory is read even faster with the default options (no
|
||||
[parser callback](../../api/basic_json/parser_callback_t.md), no comments, no trailing commas, and for `sax_parse()`
|
||||
`strict = true`): `parse()`, `accept()`, and `sax_parse()` then read the grammar and the tokens in one loop over the
|
||||
memory. Only after an error do `parse()` and `sax_parse()` read the input again, to report the error exactly as before.
|
||||
|
||||
Parsing a file from a `#!cpp std::ifstream` therefore takes about 1.15 to 1.45 times as long as reading the whole file
|
||||
into a `#!cpp std::string` first and parsing that, but it needs no memory for a copy of the file. The stream is never read
|
||||
ahead: after parsing, it is positioned exactly as if every character had been read one at a time, so
|
||||
[`operator>>`](../../api/operator_gtgt.md) can still read several values from one stream (see also
|
||||
[JSON Lines](json_lines.md)).
|
||||
|
||||
@@ -29,9 +29,9 @@ up. Nothing is read ahead, so the stream is left exactly where it would be after
|
||||
[`operator>>`](../api/operator_gtgt.md) can still read several values from one stream. A stream buffer without a get
|
||||
area is read one character at a time. A `#!cpp FILE*` is still read one character at a time, but `file_input_adapter`
|
||||
locks the file once per parse instead of once per character. See
|
||||
[input sources and performance](parsing/index.md#input-sources-and-performance) for the full table. According to the
|
||||
description of [#5828](https://github.com/nlohmann/json/pull/5828), parsing from a stream is within about 10 to 20 % of
|
||||
parsing an equivalent `#!cpp std::string`.
|
||||
[input sources and performance](parsing/index.md#input-sources-and-performance) for the full table. Parsing from a
|
||||
stream takes about 1.15 to 1.45 times as long as parsing an equivalent `#!cpp std::string`, which `parse()` reads with
|
||||
a parser of its own (see there).
|
||||
|
||||
Inputs that are still read one character at a time: iterator ranges that are not contiguous (e.g.
|
||||
`#!cpp std::list<char>` or `#!cpp std::istreambuf_iterator<char>`), wide strings, and user-defined adapters. An
|
||||
|
||||
Reference in new issue
Block a user