* Add json_document and json_view: node index, parser, and document Add json_document and json_view, a read-only, zero-copy index of a JSON text, as the first public slice of the zero-copy view (#5295). A parse produces a flat array of 16-byte nodes in document order, one per value and one per object key. Strings stay in the source text; escaped strings are decoded into an arena. Integers are converted while their digits are in the cache; floats keep only their digit layout and are converted on read. Containers store the size of their subtree, so a reader can step over one in constant time. A document makes a handful of allocations, however many values it has. The parser accepts exactly what json::parse accepts, with every combination of ignore_comments and ignore_trailing_commas, with and without a trailing NUL, and under JSON_STRICT_NUL_HANDLING. It is portable C++11 and does not depend on byte order. basic_json_document adds parse, parse_copy, accept, read (reuses a document's memory), root, is_discarded, source, owns_source, node_count, memory_usage, and shrink_to_fit. basic_json_view adds type, the is_* queries, operator bool, size, empty, materialize, and source_offset. A parse error throws the same exception basic_json::parse would throw for the same input, message and position included. detail::abi_config keeps JSON_STRICT_NUL_HANDLING and JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON readable after json.hpp undefines them, in the ABI namespace so they always match the basic_json in use. A NUL byte that ends a // comment is the end of the input, as in parse() since #5696. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix document_data for old Clang (3.4-3.6) Drop the empty braced NSDMIs of the std::string members: old Clang rejects the defaulted constructor when it is used by a member initializer before the end of the class definition. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Exclude libstdc++'s string_view comparison from the sign-change check basic_string_view::_S_compare stores the difference of two lengths in a signed difference_type on purpose, which -fsanitize=integer reports for every comparison with a shorter view (test-json_view_cpp17). Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Reject integer lengths in json_document::parse parse(ptr, len) compiled: len converted to allow_exceptions, and ptr was read as a C string, past the end of a buffer without a terminating NUL. Delete the overloads of parse, parse_copy, accept, and read that take an integer other than bool where the flags are expected. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Copy const rvalue strings and document the accepted inputs json_document::parse(std::move(const_string)) failed to compile with "no matching read_kind": only a non-const rvalue std::string can be moved from. Treat a const rvalue as a copied byte container. parse() does not accept everything BasicJsonType::parse() does: a FILE* and pointers to or arrays of wide characters are rejected at compile time. List the supported inputs instead. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Remove the conversion to bool from json_view explicit operator bool meant "refers to a value", which silently differs from what a basic_json converts to. Use !v.is_discarded() instead. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Delete json_document::root() on temporary documents auto v = json_document::parse(text).root() compiled and left the view dangling. Delete the overload for rvalue documents, take a named document in the tests, and document the lifetime rule. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Store node integers by halves on big-endian targets The non-little-endian path of the node writer wrote the integer's native word over len and next, so len got the high half there. Compose and split the value explicitly (len is the low half, next the high half); the little-endian path stays a plain memcpy. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Check the node array size for overflow reserve(n) and the growth of the index computed n * sizeof(node) without a check, which wraps around on 32-bit targets for inputs of about 1 GiB and allocates a too small array. Throw std::bad_alloc for a count beyond the address space and clamp the growth step to it. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * State the exact input size limit of json_document The check rejects inputs of 0xFFFFFFF0 bytes or more, but the exception message and the documentation said 4 GiB. Name the limit once (max_input_size), and state 4 GiB minus 16 bytes in the message and the documentation. Test the limit with a container that only claims the size. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Remove the unused legacy_discarded_value_comparison from abi_config Nothing reads it: the view does not compare values. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Align the banner comment of json_view.hpp Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fuzz json_document with exact-size buffers and parse options The fuzzer only parsed a std::string with default options. Also parse an exact-size byte vector, which has no NUL after its last byte and takes the bounds-checked path, and derive ignore_comments and ignore_trailing_commas from the first input byte, comparing against json::parse and json::accept with the same options. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Give document_data a user-provided constructor Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix clang-tidy findings in unit-json_view_builder.cpp Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Annotate intentional patterns in unit-json_view.cpp for clang-tidy Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Suppress Flawfinder's read() finding on the deleted overload Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Check the steps of the deep-nesting test separately Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Make max_input_size a function (GCC -Wunused-const-variable) Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Make json_view.hpp pass include-what-you-use ci_single_binaries runs IWYU with --error on every header. For json_view.hpp it suggested adding <array> (std::array is used), the headers that json.hpp already provides (abi_config, abi_macros, input_adapters, json_pointer, cpp_future, string_concat, value_t, json_fwd), and <version> for std::nullptr_t, and removing <cstddef>. - include <array> - keep <cstddef> (nullptr_t, size_t; IWYU attributes them to <version> and <cstring>) - tell IWYU not to suggest the headers that json.hpp provides: the amalgamated json_view.hpp only includes json.hpp, so including them here would duplicate their definitions - export json.hpp, keep macro_unscope.hpp, and drop the unused forward declaration of the document class Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Read the node array base after emit() in the view builder's open() emit() moves the node array when it grows, and the subtraction read base in the same expression, so the order was unspecified. MSVC Release builds without forced inlining read the old base; the container index then pointed outside the array and close() wrote out of bounds. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Silence a clang-tidy use-after-move finding in the json_view test const_text is const, so std::move does not move from it; the second call is part of the const rvalue test. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Regenerate the amalgamated headers Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Address the review of the view builder and document Changes promised in the replies to the review of #5620: - read() reuses the owned copy: a copied input (an rvalue byte container, a stream, FILE*, wide characters, an iterator range) is appended to the cleared owned buffer instead of a fresh std::string being move-assigned over it. A moved-in std::string still replaces it. - The error replay runs the library parser with a SAX handler that stores nothing and throws the reported exception, instead of BasicJsonType::parse(), which built a DOM up to the error. - Pointers to and arrays of wide characters, and FILE*, take the input adapter path (converted to UTF-8 into an owned buffer) instead of failing a static_assert. Wide pointers are NUL-terminated. - Streams are read in blocks through rdbuf() into the buffer, instead of character by character through the input adapter; eofbit is set at the end, as by the library's stream adapter. - tape/tape_size/tape_cap are now nodes/node_count/node_capacity. - The builder's members get descriptive, m_-prefixed names. - The shallow frame stack is a std::array (in a wrapper with a user-provided constructor: -Weffc++ wants the member initialized, and value-initializing the 64 frames costs up to 30 % on small documents). - The value dispatch macro has no NEXT parameter; the switch is left with break, and the macro is undefined after its last use. - literal() and digits_exceed() deduce the length from the string literal; each literal has one helper; the two `if (!is_float)` blocks are merged. - arena_ensure() sits right before the writes; the decode arena is a std::vector<char>. - The builder records the deepest nesting, and materialize() sizes its stack from it once (growing it only if a subtree is deeper). - Comment fixes (hex4, slow_string, the NUL rule, put_utf8, 0 - m, the arena resize, grow()) and formatting (macro backslashes, digit cases, one power of ten per line, value_t assertion next to is_container()). - build_with's template parameters follow the builder's order. Greg's idea of keeping the source offset in `off` and the arena offset in `next` for escaped strings was measured and not adopted: string access got 7-18 % slower with a select, and 3.5-18 % slower with a branch-free field pick (Apple M-series, clang -O3), parsing was unchanged. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Apply the review's wording fixes to the json_view documentation - Copying is disabled because of its cost, not because of two documents borrowing the same buffer; the unique_ptr follows from move-only. - Inputs: streams, FILE*, and wide characters (strings, arrays, pointers) are all supported and owned; point to parse_copy() for owned copies. - The error replay only runs for failing input, builds no values, and is skipped with allow_exceptions = false. - read() writes copied inputs into the previous owned copy. - Views can dangle like std::string_view, and why there is no shared/weak pointer design. - The 4 GiB limit (16-byte nodes, 16-byte scanner steps), streams being read to the end first, which macros apply, why materialize() has no source positions, and wording nits (accept() vs basic_json::accept(), is_binary, is_discarded, see-also lists, size table, "back into"). Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Say that json_view converts integers while parsing Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Address the review: null inline nodes, trait spelling, and max() in json_view - inline_nodes is null when the document has no inline nodes, instead of pointing past the end of the allocation; shrink_to_fit() no longer passes it to memcpy() for an empty node array - is_byte_char and is_wide_char use conjunction/disjunction, which astyle leaves unspaced - append_stream() uses (std::max) for the count read Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Include materialize.hpp in the view builder test The test calls detail::view::materialize(), which only the single header pulled in; the multiple-header build (JSON_TEST_USING_MULTIPLE_HEADERS) failed with "'materialize' is not a member of detail::view". Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Rename emit() in the view builder for Qt compatibility Qt defines emit as an empty macro, so a Qt header included before json_view.hpp broke the declaration of the builder's emit(), see issue #5865. It is now called push_node(). Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Do not copy an empty run into an unallocated arena memcpy() must not be passed a null pointer even for zero bytes, which arena_run() did for an empty run while the arena was still empty (ci_test_clang_sanitizer: "null pointer passed as argument 1" in builder.hpp). Only the slow path can see such a run. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Exclude the view builder test from Infer's reports Infer's std::unique_ptr model confuses the values that the test's build() helper returns by value and reports USE_AFTER_DELETE for one of them after the destructor of an unrelated temporary (ci_infer, 12 reports, all in tests/src/unit-json_view_builder.cpp). AddressSanitizer is clean on the same tests. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Skip the deleted-overload checks of json_view on MSVC 2015 MSVC 2015 does not treat selecting a deleted function in decltype as a substitution failure, so is_detected reports the deleted parse/parse_copy/accept/read(ptr, len) overloads as callable and the static_asserts in "integer arguments do not compile" fail (AppVeyor VS2015 jobs). Calling them still fails to compile there. Guard the negative checks as unit-delete_deprecated_functions.cpp does. Signed-off-by: Niels Lohmann <mail@nlohmann.me> --------- Signed-off-by: Niels Lohmann <mail@nlohmann.me>
16 KiB
Performance
Speed was never the primary goal of this library. The design goals page says so plainly: "There are certainly faster JSON libraries out there." Intuitive syntax, trivial integration, and thorough testing came first. If a hard real-time budget or the last percent of throughput matters more than convenience, a faster, more specialized library may be a better fit.
That said, how you use this library still makes a measurable difference. This page collects practical, code-verified techniques for reducing time, memory, and compile-time cost -- without repeating the detailed pages it links to.
Parsing input
parse accepts a string, a pair of iterators, a container, a #!cpp std::istream, or a
#!cpp FILE* (see Parsing). Internally, every input is wrapped in an
input adapter, and not all adapters are equally fast.
For inputs backed by contiguous, single-byte memory -- a #!cpp std::string, a #!cpp std::vector<char>, a string
literal, or a pointer range -- the library uses iterator_input_adapter, wrapped in a raw pointer so the fast paths
below apply on every supported standard. This adapter exposes two optimizations the lexer detects at compile time:
- it can reconstruct already-consumed input on demand for error messages, instead of copying every character as it is read, and
- the lexer can scan ordinary string characters directly out of the buffer, several bytes at a time, rather than one character (and one function call) at a time.
A #!cpp std::istream (including #!cpp std::ifstream) or #!cpp FILE*, by contrast, is read through
input_stream_adapter or file_input_adapter, which read one character (or one block, for binary formats) at a time
and expose neither optimization -- the lexer falls back to the same byte-at-a-time path it uses for any
non-contiguous, general-purpose iterator range. An iterator pair over non-contiguous but random-access storage (e.g.
#!cpp std::deque<char>::iterator) gets the first optimization but not the second, since the byte-scanning fast path
additionally requires contiguous storage.
Practically: if the JSON text is already in memory, or small enough to read into memory, prefer passing a
#!cpp std::string, a #!cpp std::vector<char>, or a pointer range to parse over a #!cpp std::istream. For a
file, that means reading it into a string first and then parsing the string, rather than passing a
#!cpp std::ifstream directly to parse -- the latter never benefits from either optimization:
// gets the contiguous fast paths
std::ifstream f("example.json");
std::string contents((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
json j = json::parse(contents);
// does not: input_stream_adapter has no fast path
std::ifstream f2("example.json");
json j2 = json::parse(f2);
For contiguous input with many non-ASCII characters, JSON_USE_SIMDUTF can
additionally speed up UTF-8 validation by using the simdutf library instead of
the built-in scalar validator; streaming inputs (files, #!cpp std::istream, wide strings, user-defined adapters)
always use the scalar path regardless of this macro.
Large documents
Parsing always produces SAX events internally; parse simply feeds them to a consumer
that builds a complete basic_json value tree (a DOM) in memory. For documents too large to comfortably hold as
a DOM, three alternatives avoid building it:
- Implement the SAX interface directly and pass it to
sax_parse; only the parts of the input you choose to keep ever becomebasic_jsonvalues. - Pass a parser callback to
parse. This still builds a DOM, but the callback can discard finished elements as soon as they are handled, so memory usage stays bounded by one element (plus the unparsed remainder of the input) instead of the whole document -- see the recipe for streaming a large homogeneous array. - Parse into a
json_document(#!cpp <nlohmann/json_view.hpp>) instead of abasic_json. It keeps the input text and builds a flat index of 16 bytes per value; strings and floating-point numbers are not copied, but read from the text when needed. Read-only views give the familiar element access, and only the parts youmaterialize()becomebasic_jsonvalues. A document that borrows the text instead of owning a copy needs the text to outlive it; see choosing betweenjson, the SAX interface, andjson_view.
If the data is naturally record-oriented, consider JSON Lines instead of one large JSON
document: reading and parsing it line by line with #!cpp std::getline means only one line's value is ever in memory
at a time, and a malformed line does not invalidate lines already processed.
Binary formats
JSON text is not a compact format. If the data is only exchanged between programs (not read by humans), the
binary formats -- BJData, BON8, BSON, CBOR, MessagePack, and UBJSON -- encode the same
values more compactly, which reduces both the bytes transferred and, for most of them, the work needed to parse them
back. The size comparison on that page, measured against minified JSON for four
reference documents, shows the effect varies a lot by document shape: CBOR and MessagePack come out at 50.5% of the
minified JSON size for the numeric-array-heavy canada.json, but only around 87-88% for the string-heavy
jeopardy.json, where there is less numeric data to encode more compactly. BON8 is the most compact option in that
comparison for text-heavy documents (63.5%-87.5%), at the cost of an
incomplete serializer (no unsigned integers above int64). Which format -- and
whether it is worth the loss of human readability at all -- depends on the actual data; see the
comparison tables before choosing one.
Object type: json vs. ordered_json
The default json type stores object keys in a #!cpp std::map, giving logarithmic-time lookup,
insertion, and erasure, at the cost of sorting keys alphabetically rather than preserving insertion order (see
Object Order). ordered_json uses
nlohmann::ordered_map instead, a #!cpp std::vector-backed container with no lookup index:
every key-based operation is a linear scan, so building an object of n distinct keys costs O(n²) in total --
this applies equally to inserting keys one by one and to parsing an object, since the parser inserts each key as it is
read. The measurements on the ordered_map page show this is
negligible at typical object sizes (2000 keys: 0.7 ms for json vs. 3.6 ms for ordered_json, a 5x factor) but grows
steeply for large, machine-generated objects (16 000 keys: 3.3 ms vs. 181.6 ms, a 54x factor).
If insertion order matters and an object routinely has many thousands of keys, ordered_json's quadratic build cost
may not be acceptable. The library's ObjectType template parameter can be
set to a different container instead: #!cpp nlohmann::fifo_map keeps insertion order with a real lookup index
(avoiding the quadratic cost), while #!cpp std::unordered_map, #!cpp boost::unordered_flat_map,
#!cpp absl::flat_hash_map, and similar hash maps trade insertion order for average-case constant-time lookup (through
an adapter, since their template argument order does not match what basic_json expects) -- see
Object Order for the full list.
Avoiding copies
- Move instead of copy. Constructing a
basic_jsonfrom an existing one is linear in its size for the copy constructor but constant for the move constructor. The same applies to assigning a large#!cpp std::string,#!cpp std::vector, or other container into a value: pass it as#!cpp std::move(x)rather thanxwheneverxis no longer needed afterwards. - Access without copying.
get<T>()returns a copy of the stored value converted toT. When a reference or pointer to the value already stored inside thebasic_jsonis enough,get_ref()andget_ptr()access it directly: both pages state, word for word, "No copies are made." -- at the cost of that reference or pointer becoming invalid once the underlying value changes. - Iterate by reference.
#!cpp basic_json::iterator::operator*()returns areference(an alias for#!cpp basic_json&), but a range-based for loop with a by-value loop variable (#!cpp for (auto el : j)) still copies each element, because plain#!cpp autodrops the reference. Write#!cpp for (const auto& el : j)(or#!cpp auto&for a mutable loop), and useitems()the same way when the key is needed too -- its own examples use#!cpp for (auto& el : j.items()). - Construct in place.
emplace_back()(arrays, amortized constant time) andemplace()(objects, logarithmic in the size of the container forjson) forward their arguments directly to abasic_jsonconstructor, rather than requiring a temporary value to be constructed and then copied or moved in.push_back()has an rvalue overload (#!cpp push_back(basic_json&&)) for a value that already exists:#!cpp j.push_back(std::move(value))moves it in instead of copying it. - Skip the bounds check when it is redundant.
at()andoperator[]have the same complexity (constant for a valid array index, logarithmic for an object key injson) -- the difference is thatat()additionally checks the key or index and throws if it is invalid, whileoperator[]does not (see unchecked access and checked access). Preferoperator[]when the surrounding code has already established that the access is valid. - Reserve array capacity.
basic_jsonhas no publicreserve(), but when building a large array incrementally with a known final size,get_ref()exposes the underlying#!cpp array_tso it can be reserved directly -- see "reserving array capacity" for the one-line recipe.
Serialization
dump() with the default #!cpp indent = -1 selects "the most compact representation"
(word for word from the page); any non-negative indent pretty-prints instead, which is more readable but produces
more bytes and more work. dump() builds and returns a complete #!cpp string_t containing the whole serialization.
operator<< writes directly to a #!cpp std::ostream instead, through the same
serializer, but without ever materializing that intermediate string -- so if the destination is a stream (a file, or
#!cpp std::cout), #!cpp os << j; avoids the allocation and copy that #!cpp os << j.dump(); would incur for large
values.
Diagnostics overhead
Two opt-in macros add diagnostic information to exceptions and to every value, at a cost that is only worth paying while it is in use:
JSON_DIAGNOSTICSadds a JSON Pointer to exception messages, pointing at the value that triggered the exception. Quoting the page directly: "enabling this macro increases the size of every JSON value by one pointer and adds some runtime overhead" -- every value gains a parent pointer that has to be kept up to date as the document is built and modified.JSON_DIAGNOSTIC_POSITIONSaddsstart_pos()andend_pos(), the byte offsets a value occupied in its parsed input. Quoting the page: "enabling this macro increases the size of every JSON value by twostd::size_tfields and adds slight runtime overhead to parsing, copying JSON value objects, and the generation of error messages for exceptions."
Both default to off. Enable them where better diagnostics are worth the overhead (for example, while validating untrusted input, or in a debug build), and keep them off in a release build that does not need them.
Compile time
<nlohmann/json_fwd.hpp> forward-declares
basic_json, json, ordered_json,
json_pointer, and adl_serializer, pulling in only
a handful of lightweight standard headers instead of the full json.hpp. A header that only needs to name
nlohmann::json -- in a function signature or a class member declaration, for instance -- can include json_fwd.hpp
and leave #!cpp #include <nlohmann/json.hpp> to the source files that actually parse, build, or serialize values,
the same way a project would forward-declare any other heavy class to keep it out of widely-included headers:
// my_type.hpp
#include <nlohmann/json_fwd.hpp>
class my_type
{
nlohmann::json config() const;
};
// my_type.cpp
#include <nlohmann/json.hpp>
#include "my_type.hpp"
nlohmann::json my_type::config() const { /* ... */ }
One caveat: ABI-affecting macros such as JSON_DIAGNOSTICS and JSON_DIAGNOSTIC_POSITIONS are encoded into the
library's inline namespace name. Every translation unit -- whether it includes
json_fwd.hpp or the full header -- must define them the same way, or linking fails with undefined references
instead of a compile error.
If I/O support is not needed at all, JSON_NO_IO excludes <cstdio>, <ios>,
<iosfwd>, <istream>, and <ostream> outright and drops the #!cpp std::istream/#!cpp FILE* parse overloads and
operator<< that depend on them (dump() itself is unaffected, since it only returns a
string); it exists for environments where those headers are unavailable (such as Intel SGX), and as a side effect
those headers are then never processed by the compiler at all.
See also
- Design goals - why this library does not optimize for speed first
- Architecture - how input adapters, the lexer, and the serializer fit together
- Parsing - the available parsing functions and inputs
- SAX interface - parse without building a DOM
- Zero-copy JSON views - parse into a flat index of the text and read it without building a DOM
- Binary formats - compact alternatives to JSON text
- Object Order -
jsonvs.ordered_jsonand otherObjectTypechoices - Template Parameter Requirements - custom container and allocator types
- Supported macros - overview of all configuration macros, including the diagnostics ones above