Make parsing from std::istream and FILE* fast

Squashed onto develop cd6b428ef (CI fixes from #5860). Original commits:

- Let the lexer's bulk fast paths run on a window of the input
- Scan the get area of an input stream in bulk
- Lock a FILE once per parse instead of once per character
- Add benchmarks for parsing from a FILE* and for many values from a stream
- Avoid implicit unsigned char to char conversion in stream tests
- Make file_input_adapter::unlock() safe to call on a moved-from adapter

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann committed 2026-10-16 19:26:50 +02:00
1 parent cd6b428ef0
commit 1871d09af0
22 files changed
+2107 -67

No files matched your search

@@ -132,6 +132,10 @@ outside of a string, invalid) byte; see the [FAQ entry](../../home/faq.md#nul-by
- [operator>>](../operator_gtgt.md) - deserialize from stream
- [`JSON_STRICT_NUL_HANDLING`](../macros/json_strict_nul_handling.md) - opt in to rejecting a NUL byte in the input
instead of treating it as end of input
- [`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md) - read a `std::istream` one character
at a time
- [`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md) - read a `FILE*` with the locking stdio
functions
## Version history
@@ -143,6 +147,10 @@ outside of a string, invalid) byte; see the [FAQ entry](../../home/faq.md#nul-by
- Extended overload (2) to accept heterogeneous iterator+sentinel pairs (C++20 ranges support) in version 3.13.0.
- `JSON_STRICT_NUL_HANDLING` added in version 3.13.0 to optionally reject a NUL byte in the input instead of treating
it as end of input; planned to become the default in version 4.0.0.
- Parsing from an `std::istream` scans the characters in its stream buffer in bulk in version 3.13.0
([`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md)).
- Parsing from a `FILE*` locks the file once instead of once per character in version 3.13.0
([`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md)).
!!! warning "Deprecation"
+19
View File
@@ -112,6 +112,16 @@ By default, a `'\0'` (NUL) byte anywhere in the input is treated as end of input
outside of a string, invalid) byte; see the [FAQ entry](../../home/faq.md#nul-bytes-in-the-input) for details and the
[`JSON_STRICT_NUL_HANDLING`](../macros/json_strict_nul_handling.md) macro to opt into rejecting it instead.
!!! tip "Performance"
Contiguous inputs (strings, string views, vectors, arrays, pointer ranges) are the fastest to parse. A
`#!cpp std::istream` comes close: the parser scans the characters its stream buffer holds in bulk, without reading
ahead, so the stream is left exactly where it would be after reading one character at a time. A `#!cpp FILE*` is
read one character at a time, but the file is locked only once, so other threads that use the same `FILE` object
wait until parsing is done. See [input sources and performance](../../features/parsing/index.md#input-sources-and-performance),
[`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md), and
[`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md).
## Examples
??? example "Example: (1) parse from a character array"
@@ -247,6 +257,10 @@ outside of a string, invalid) byte; see the [FAQ entry](../../home/faq.md#nul-by
- [operator>>](../operator_gtgt.md) - deserialize from stream
- [`JSON_STRICT_NUL_HANDLING`](../macros/json_strict_nul_handling.md) - opt in to rejecting a NUL byte in the input
instead of treating it as end of input
- [`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md) - read a `std::istream` one character
at a time
- [`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md) - read a `FILE*` with the locking stdio
functions
## Version history
@@ -261,6 +275,11 @@ outside of a string, invalid) byte; see the [FAQ entry](../../home/faq.md#nul-by
it as end of input; planned to become the default in version 4.0.0.
- Extended empty-input detection to also cover an `std::istream` without a stream buffer, and fixed a crash
(`std::terminate`) when parsing from an `std::istream` with `eofbit` in its exception mask, in version 3.13.0.
- Parsing from an `std::istream` scans the characters in its stream buffer in bulk in version 3.13.0
([`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md) restores reading one character at a
time).
- Parsing from a `FILE*` locks the file once instead of once per character in version 3.13.0
([`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md) restores `std::fgetc`).
!!! warning "Deprecation"
@@ -116,6 +116,10 @@ consumer `sax` has a super-linear complexity.
A UTF-8 byte order mark is silently ignored.
When reading from a `#!c FILE*`, the file is locked while it is parsed. The lock is recursive, so the SAX handler may
use the same file with stdio functions, but it must not wait for another thread that uses the same `FILE` object; see
[`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md).
## Examples
??? example
@@ -137,6 +141,10 @@ A UTF-8 byte order mark is silently ignored.
- [parse](parse.md) - deserialize from a compatible input
- [accept](accept.md) - check if the input is valid JSON
- [`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md) - read a `std::istream` one character
at a time
- [`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md) - read a `FILE*` with the locking stdio
functions
## Version history
@@ -148,6 +156,10 @@ A UTF-8 byte order mark is silently ignored.
- Extended overload (2) to accept heterogeneous iterator+sentinel pairs (C++20 ranges support) in version 3.13.0.
- `JSON_PRECISE_STREAM_POSITION` added in version 3.13.0 to optionally leave a `#!cpp std::istream` positioned right
after the parsed value when `strict` is `#!cpp false`.
- Parsing from an `std::istream` scans the characters in its stream buffer in bulk in version 3.13.0
([`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md)).
- Parsing from a `FILE*` locks the file once instead of once per character in version 3.13.0
([`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md)).
!!! warning "Deprecation"
+4
View File
@@ -16,6 +16,10 @@ header. See also the [macro overview page](../../features/macros.md).
## Parsing
- [**JSON_DISABLE_STREAM_BULK_SCAN**](json_disable_stream_bulk_scan.md) - read a `std::istream` one character at a time
instead of scanning its stream buffer in bulk
- [**JSON_DISABLE_UNLOCKED_FILE_IO**](json_disable_unlocked_file_io.md) - read a `FILE*` with `std::fgetc` instead of
locking it once and using the unlocked stdio functions
- [**JSON_PRECISE_STREAM_POSITION**](json_precise_stream_position.md) - opt in to leaving an input stream positioned
right after a parsed number
- [**JSON_STRICT_BINARY_UTF8**](json_strict_binary_utf8.md) - opt in to checking strings for valid UTF-8 in the CBOR,
@@ -0,0 +1,145 @@
# JSON_DISABLE_STREAM_BULK_SCAN
```cpp
#define JSON_DISABLE_STREAM_BULK_SCAN /* value */
```
When defined to `1`, parsing from a `#!cpp std::istream` reads the input one character at a time through the stream
buffer, as in version 3.12.0 and earlier, instead of scanning the characters that the stream buffer already holds in
bulk.
The macro is an escape hatch for stream buffers that do not follow the usual contract of `std::basic_streambuf` (see
the requirements on the stream buffer in the notes below). It does not change any result: values, exceptions, and the
position of the stream after parsing are the same either way. Only the speed differs.
## Default definition
The default value is `0`: streams are scanned in bulk.
```cpp
#define JSON_DISABLE_STREAM_BULK_SCAN 0
```
## Notes
!!! note "How streams are scanned"
A `std::streambuf` keeps the part of the input it has read but not yet handed out in its *get area*, the range
from `gptr()` to `egptr()`. A `std::filebuf`, for instance, reads the file in blocks of a few kilobytes into its get
area, and a `std::stringbuf` exposes its whole string as one get area.
When parsing from a `std::istream`, the parser scans this get area directly, with the same fast paths that it uses
for a `std::string`: runs of ordinary string characters, numbers, and indentation are recognized several bytes at a
time instead of being requested from the stream buffer one character at a time. When the parser reaches the end of
the get area, it asks the stream buffer for the next character as before, which makes the stream buffer refill its
get area.
The parser only reads characters that it would also have read one at a time, and it advances the stream buffer
exactly over the characters it consumes. Nothing is read ahead, buffered elsewhere, or put back. Therefore, the
stream is left exactly where it was left before:
- [`operator>>`](../operator_gtgt.md) and [`sax_parse`](../basic_json/sax_parse.md) with `strict = false` can still
read several values from one stream, and the stream can still be used with other `std::istream` functions after a
value has been read.
- With [`JSON_PRECISE_STREAM_POSITION`](json_precise_stream_position.md), the stream is still positioned right after
a parsed number.
- Exception messages, including the line, the column, and the last read token, are unchanged.
!!! 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.
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.
!!! note "Requirements on the stream buffer"
The bulk scan relies on the following behavior, which every stream buffer of the standard library and every stream
buffer that keeps its input in its get area has:
- When `sgetc()` returns a character while `gptr() < egptr()`, that character is `*gptr()`; when `sbumpc()` returns
one, it was at `gptr()` before the call.
- Characters in the get area are not changed while they are in the get area.
- `underflow()` or `uflow()` either sets up a get area that holds the returned character at `gptr()` (or at
`gptr() - 1` after `uflow()`), or leaves the get area empty.
A stream buffer that returns characters from `uflow()` while leaving unrelated characters in its get area, or that
changes the characters in its get area in place, does not meet these requirements. Define the macro to `1` for
such stream buffers.
!!! warning "Define consistently"
The macro selects between two definitions of the library's internal stream input adapter. It must therefore be
defined identically for **every** translation unit that includes the library; mixing translation units that define
it with ones that do not is an ODR violation. Prefer setting it as a compile definition on the target rather than
with `#!cpp #define` in individual source files.
!!! note "Other inputs"
Strings, string views, vectors, arrays, and pointer or iterator ranges over contiguous memory are scanned in bulk
anyway. Reading from a `#!cpp FILE*` is not affected by this macro; see
[`JSON_DISABLE_UNLOCKED_FILE_IO`](json_disable_unlocked_file_io.md) for that input.
## Examples
??? example "Example: reading values from a stream (default)"
The stream is scanned in bulk, and the values are read one after the other:
```cpp
#include <iostream>
#include <sstream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
int main()
{
std::istringstream input(R"({"id": 1} {"id": 2} 42 rest)");
json j1, j2, j3;
input >> j1 >> j2 >> j3; // {"id":1}, {"id":2}, 42
std::string word;
input >> word; // "rest"
}
```
??? example "Example: reading one character at a time (macro defined to 1)"
With the macro, the same program reads the stream one character at a time, with the same results:
```cpp
#define JSON_DISABLE_STREAM_BULK_SCAN 1
#include <iostream>
#include <sstream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
int main()
{
std::istringstream input(R"({"id": 1} {"id": 2} 42 rest)");
json j1, j2, j3;
input >> j1 >> j2 >> j3; // {"id":1}, {"id":2}, 42
std::string word;
input >> word; // "rest"
}
```
## See also
- [**parse**](../basic_json/parse.md) - deserialize from a compatible input
- [**operator>>**](../operator_gtgt.md) - deserialize from stream
- [**sax_parse**](../basic_json/sax_parse.md) - generate SAX events
- [**JSON_PRECISE_STREAM_POSITION**](json_precise_stream_position.md) - leave an input stream positioned right after a
parsed number
- [**JSON_DISABLE_UNLOCKED_FILE_IO**](json_disable_unlocked_file_io.md) - read a `FILE*` with the locking stdio
functions
- [Parsing: input sources and performance](../../features/parsing/index.md#input-sources-and-performance)
## Version history
- Added in version 3.13.0.
@@ -0,0 +1,113 @@
# JSON_DISABLE_UNLOCKED_FILE_IO
```cpp
#define JSON_DISABLE_UNLOCKED_FILE_IO /* value */
```
When defined to `1`, parsing from a `#!cpp FILE*` reads every character with `std::fgetc`, as in version 3.12.0 and
earlier, instead of locking the file once and reading it with the unlocked stdio functions.
It does not change any result: values, exceptions, and the position of the file after parsing are the same either way.
Only the speed and the locking of the file differ (see the notes on locking below).
## Default definition
The default value is `0`: the file is locked once and read with the unlocked stdio functions where they are available.
```cpp
#define JSON_DISABLE_UNLOCKED_FILE_IO 0
```
## Notes
!!! note "Background"
The C standard library makes every `std::fgetc` call thread-safe by locking and unlocking the `FILE` object. When
parsing from a `FILE*`, the parser reads one character at a time, so this happens for every character of the input.
Instead, the parser locks the file once when parsing starts and unlocks it when parsing ends - also when an
exception is thrown - and reads the characters in between without locking:
| C library | functions |
|------------------------------------------------------------|----------------------------------------------------|
| glibc (Linux), Apple (macOS, iOS), Android, FreeBSD, NetBSD, OpenBSD, DragonFly BSD | `flockfile`, `getc_unlocked`, `funlockfile` (POSIX) |
| Microsoft (MSVC, clang-cl) | `_lock_file`, `_fgetc_nolock`, `_unlock_file` |
| others (e.g., musl, MinGW, newlib) | `std::fgetc`, as before |
This makes parsing from a `FILE*` 3 to 4 times as fast with the Apple C library and 1.1 to 1.3 times as fast with
glibc, whose locking is cheaper. The binary formats ([`from_cbor`](../basic_json/from_cbor.md),
[`from_msgpack`](../basic_json/from_msgpack.md), ...) read from a `FILE*` the same way.
!!! note "Locking"
While a value is parsed from a `FILE*`, the parsing thread holds the file's lock. The lock is recursive, so the
parsing thread itself - for instance, a [SAX](../json_sax/index.md) handler or a
[parser callback](../basic_json/parser_callback_t.md) - can keep using the file with stdio functions such as
`std::ftell`. Other threads that use the same `FILE` object wait until the parser is done. Before, they could
interleave their reads with the parser's, which made the parsed input unpredictable anyway.
A program in which a SAX handler or parser callback waits for **another** thread that uses the **same** `FILE`
object would deadlock. Define the macro to `1` for such programs.
!!! warning "Define consistently"
The macro selects between two definitions of the library's internal `FILE*` input adapter. It must therefore be
defined identically for **every** translation unit that includes the library; mixing translation units that define
it with ones that do not is an ODR violation (and can leave a file locked). Prefer setting it as a compile
definition on the target rather than with `#!cpp #define` in individual source files.
!!! note "Other inputs"
Reading from a `#!cpp std::istream` is not affected by this macro; see
[`JSON_DISABLE_STREAM_BULK_SCAN`](json_disable_stream_bulk_scan.md) for that input.
## Examples
??? example "Example: parsing from a `FILE*` (default)"
The file is locked once while it is parsed:
```cpp
#include <cstdio>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
int main()
{
std::FILE* f = std::fopen("example.json", "rb");
json j = json::parse(f); // f is locked until parse() returns
std::fclose(f);
}
```
??? example "Example: locking for every character (macro defined to 1)"
With the macro, every character is read with `std::fgetc`, with the same result:
```cpp
#define JSON_DISABLE_UNLOCKED_FILE_IO 1
#include <cstdio>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
int main()
{
std::FILE* f = std::fopen("example.json", "rb");
json j = json::parse(f); // std::fgetc for every character
std::fclose(f);
}
```
## See also
- [**parse**](../basic_json/parse.md) - deserialize from a compatible input
- [**sax_parse**](../basic_json/sax_parse.md) - generate SAX events
- [**JSON_DISABLE_STREAM_BULK_SCAN**](json_disable_stream_bulk_scan.md) - read a `std::istream` one character at a
time
- [Parsing: input sources and performance](../../features/parsing/index.md#input-sources-and-performance)
## Version history
- Added in version 3.13.0.
@@ -72,6 +72,12 @@ The default value is `0` (disabled — existing behavior is preserved).
distinct symbol names. Translation units compiled with and without it can therefore be linked into the same program
without One Definition Rule (ODR) violations, but they cannot exchange instances of library types.
!!! note "Bulk scanning"
The parser scans the characters in the stream buffer in bulk (see
[`JSON_DISABLE_STREAM_BULK_SCAN`](json_disable_stream_bulk_scan.md)), but it never reads ahead. The stream is left
at the same position with and without bulk scanning, in both modes of this macro.
!!! tip "Workaround without the macro"
Separate the values in the stream with whitespace. The character consumed after a number is then the separator,
@@ -12,8 +12,9 @@ non-ASCII characters (e.g. CJK or emoji) this can validate several times faster.
This is an **opt-in external dependency**. The library itself remains header-only and its behavior is unchanged: the
same input is accepted or rejected either way, and every parse error is reported at the same position with the same
message (simdutf is only used to fast-path *valid* runs; anything it flags falls back to the scalar path so the exact
diagnostic is preserved). Streaming inputs (files, `std::istream`, wide strings, user-defined adapters) always use the
scalar path.
diagnostic is preserved). A `std::istream` uses simdutf for the characters its stream buffer holds (see
[`JSON_DISABLE_STREAM_BULK_SCAN`](json_disable_stream_bulk_scan.md)); other streaming inputs (`FILE*`, wide strings,
user-defined adapters) always use the scalar path.
When `JSON_USE_SIMDUTF` is defined you must make the `simdutf.h` header available on the include path and link the
simdutf library. When it is not defined, no simdutf header is included and there is no dependency.
+4
View File
@@ -121,6 +121,7 @@ being read.
- [`JSON_STRICT_NUL_HANDLING`](macros/json_strict_nul_handling.md) - opt in to rejecting a NUL byte in the input
instead of treating it as end of input
- [`JSON_PRECISE_STREAM_POSITION`](macros/json_precise_stream_position.md) - opt in to leaving the stream positioned right after a number
- [`JSON_DISABLE_STREAM_BULK_SCAN`](macros/json_disable_stream_bulk_scan.md) - read the stream one character at a time
## Version history
@@ -133,3 +134,6 @@ being read.
crash (`std::terminate`) when `i` has `eofbit` in its exception mask, in version 3.13.0.
- Changed to the strong exception safety guarantee in version 3.13.0: `j` is no longer left with a partially parsed
value if parsing throws.
- Scans the characters in the stream buffer of `i` in bulk in version 3.13.0; the position of `i` after the call is
unchanged ([`JSON_DISABLE_STREAM_BULK_SCAN`](macros/json_disable_stream_bulk_scan.md) restores reading one character
at a time).
+16
View File
@@ -133,6 +133,14 @@ When defined, default parse and serialize functions for enums are excluded and h
See [full documentation of `JSON_DISABLE_ENUM_SERIALIZATION`](../api/macros/json_disable_enum_serialization.md).
## `JSON_DISABLE_STREAM_BULK_SCAN`
When defined to `1`, parsing from a `#!cpp std::istream` reads one character at a time instead of scanning the
characters that the stream buffer holds in bulk. Results are the same either way; the macro is an escape hatch for
stream buffers that do not follow the usual `std::streambuf` contract. The default value is `0`.
See [full documentation of `JSON_DISABLE_STREAM_BULK_SCAN`](../api/macros/json_disable_stream_bulk_scan.md).
## `JSON_DISABLE_TUPLE_REFERENCE_CONVERSION`
When defined to `1`, a JSON value can no longer be created from a one-element `std::tuple` holding a reference to a JSON
@@ -141,6 +149,14 @@ is planned to become the default in version 4.0.0.
See [full documentation of `JSON_DISABLE_TUPLE_REFERENCE_CONVERSION`](../api/macros/json_disable_tuple_reference_conversion.md).
## `JSON_DISABLE_UNLOCKED_FILE_IO`
When defined to `1`, parsing from a `#!cpp FILE*` reads every character with `std::fgetc`, which locks the file each
time, instead of locking the file once and reading it with the unlocked stdio functions. Results are the same either
way. The default value is `0`.
See [full documentation of `JSON_DISABLE_UNLOCKED_FILE_IO`](../api/macros/json_disable_unlocked_file_io.md).
## `JSON_NO_AUTOMATIC_UDLS`
When defined, `<nlohmann/json.hpp>` does not include `<nlohmann/json_literals.hpp>` with the user-defined string literals
@@ -55,6 +55,30 @@ whitespace, because `operator>>` consumes the character that terminates a number
[`JSON_PRECISE_STREAM_POSITION`](../../api/macros/json_precise_stream_position.md) is defined to `1` — see the
[`operator>>` notes](../../api/operator_gtgt.md#notes) for details and examples.
## Input sources and performance
All inputs produce the same values and the same exceptions, but they are read in different ways, which makes some of
them faster than others:
| input | how it is read |
|--------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------|
| strings, string views, string literals, contiguous containers, pointer or iterator ranges over contiguous memory | the whole input is scanned in bulk: runs of string characters, numbers, and indentation are recognized several bytes at a time |
| `#!cpp std::istream` (`#!cpp std::ifstream`, `#!cpp std::istringstream`, ...) | the characters the stream buffer holds are scanned in bulk like a string; the stream buffer is asked for more when they are used up |
| `#!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
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)).
Two macros switch these optimizations off without changing any result:
[`JSON_DISABLE_STREAM_BULK_SCAN`](../../api/macros/json_disable_stream_bulk_scan.md) for stream buffers that do not
follow the usual `std::streambuf` contract, and
[`JSON_DISABLE_UNLOCKED_FILE_IO`](../../api/macros/json_disable_unlocked_file_io.md) for programs in which another
thread must use the same `#!cpp FILE` object while it is parsed.
## SAX vs. DOM parsing
The library offers two parsing models: