Repository navigation
Conversation
🔴 Amalgamation check failed! 🔴The source code has not been amalgamated and/or formatted correctly, or |
50f0a1b to
d33586c
Compare
da1c822 to
d4ac1b7
Compare
d4ac1b7 to
1f0c3be
Compare
e4f4c76 to
6ee5e89
Compare
6ee5e89 to
8daec2b
Compare
71985e0 to
a434c52
Compare
| 1. Linear in the number of members: as for [`ordered_json`](../ordered_json.md), members are compared one after | ||
| another, in document order, stopping at the first match. Each comparison first checks the | ||
| key's length -- already known from the index, without reading the key bytes -- before comparing its content. | ||
| Objects with 128 or more members get a hash index while parsing, so that a lookup in them takes constant time |
There was a problem hiding this comment.
clarify that this is for keys?
| 1. Linear in the number of members: as for [`ordered_json`](../ordered_json.md), members are compared one after | ||
| another, in document order, stopping at the first match. Each comparison first checks the | ||
| key's length -- already known from the index, without reading the key bytes -- before comparing its content. | ||
| Objects with 128 or more members get a hash index while parsing, so that a lookup in them takes constant time |
There was a problem hiding this comment.
clarify that this is for keys?
| Linear in the number of members: as for [`ordered_json`](../ordered_json.md), members are compared one after another, | ||
| in document order, stopping at the first match. Each comparison first checks the key's length | ||
| -- already known from the index, without reading the key bytes -- before comparing its content. | ||
| Objects with 128 or more members get a hash index while parsing, so that a lookup in them takes constant time on |
There was a problem hiding this comment.
clarify that this is for keys?
| Linear in the number of members: as for [`ordered_json`](../ordered_json.md), members are compared one after another, | ||
| in document order, stopping at the first match. Each comparison first checks the key's length | ||
| -- already known from the index, without reading the key bytes -- before comparing its content. | ||
| Objects with 128 or more members get a hash index while parsing, so that a lookup in them takes constant time on |
There was a problem hiding this comment.
clarify that this is for keys?
| another, in document order, stopping at the first match. Each comparison first checks the | ||
| key's length -- already known from the index, without reading the key bytes -- before comparing its content, so a | ||
| key of a different length than `key` is rejected without touching the source text. | ||
| Objects with 128 or more members get a hash index while parsing, so that a lookup in them takes constant time |
There was a problem hiding this comment.
clarify that this is for keys?
| for (const node* k = document_data::first_child(obj), *end = document_data::child_end(obj); k != end; k = document_data::after(k + 1)) | ||
| { | ||
| const char* const key = d.str(*k); | ||
| const std::uint64_t hash = key_hash(key, k->len); // (a cast of the call would be useless where std::uint64_t is std::size_t) |
There was a problem hiding this comment.
there's a conditional cast for that
| duplicate = true; // keep the first | ||
| break; | ||
| } | ||
| if (++distance > index_max_displacement) |
There was a problem hiding this comment.
Don't need to combine the increment and the test, and don't need a flag, just return.
if (distance == index_max_displacement)
{
d.index_slots.resize(start); // no table: the object is searched linearly
return;
}
++distance;
| d.index_slots.resize(start); // no table: the object is searched linearly | ||
| return; | ||
| } | ||
| d.indexes.push_back(document_data::object_index{start, static_cast<std::uint32_t>(mask)}); |
There was a problem hiding this comment.
Same as in the builder: object_index is an aggregate, so emplace_back(start, mask) doesn't compile before C++20.
— posted by Claude Code on behalf of @nlohmann
| { | ||
| const document_data::object_index& ix = d.indexes[obj->extra - 1u]; | ||
| const std::uint32_t* const table = d.index_slots.data() + ix.start; | ||
| const std::uint64_t hash = key_hash(key, n); // (a cast of the call would be useless where std::uint64_t is std::size_t) |
| const document_data::object_index& ix = d.indexes[obj->extra - 1u]; | ||
| const std::uint32_t* const table = d.index_slots.data() + ix.start; | ||
| const std::uint64_t hash = key_hash(key, n); // (a cast of the call would be useless where std::uint64_t is std::size_t) | ||
| std::size_t i = static_cast<std::size_t>(hash) & ix.mask; |
There was a problem hiding this comment.
use hash_index and leave i for the for loop index?
There was a problem hiding this comment.
Is this a Qt collision? (I know having to deal with such broad Qt macros is not pleasant.)
6666f55 to
4cdc02d
Compare
Speed up json_view's parser with SIMD scanning and a hash table for large objects. Long runs of string bytes are scanned 16 bytes at a time with NEON (AArch64, GCC and Clang) and SSE2 (x86-64), both baseline instruction sets. Keys keep 16 table checks before the vector loop, because their lengths repeat from record to record; string values get 8, because their lengths vary more. Non-ASCII text is validated 16 bytes at a time with simdjson's "lookup4" check (Keiser and Lemire, 2021), with NEON on AArch64 and, on x86-64, with SSSE3. SSSE3 is not part of baseline x86-64, so the check is compiled for SSSE3 with a function attribute and used only where CPUID reports it, which all x86-64 CPUs since about 2011 do; the answer is cached in a statically initialized atomic, so there is no guard of a local static and no global constructor. The same input is accepted either way. JSON_VIEW_NO_SIMD selects the portable code. On x86-64, string runs are now checked vector-first: one SSE2 compare from the first byte finds the end of most keys and short values, instead of a branch per byte for the first 8-16 bytes. AArch64 keeps the byte-wise steps, where a NEON mask costs more and the branches predict well. Entering an object or array no longer stalls: open() stores the parent's frame field by field instead of building it on the stack and reading it back with wider loads, which waited for the narrower stores to retire. Objects with 128 members or more get an open-addressing hash table built when the object closes, so operator[], at(), find(), contains(), count(), value(), and JSON pointers take constant time on average in such objects; of duplicate keys, the first is kept, as for the linear search. The idea comes from Boost.JSON. simdjson is credited in simd.hpp's SPDX block, the README, and license.md. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
GCC ignores the target attribute in modules, so the SSSE3 dispatch is disabled for the module interface (the check stays portable, SSE2 is kept). Make the 8-vs-16 byte unrolling condition in scan_string_run a preprocessor/template split to avoid a constant condition (C4127). Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
The key hash is not seeded, so keys chosen to collide made building the table quadratic (20,000 colliding keys took 470 ms to parse). A key may now sit at most 64 slots from its home slot; if a key would sit further away, the table is dropped and the object is searched linearly. Lookups stop after the same distance. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
shrink_to_fit() now trims the tables of large objects like the node array and the decoded strings, and the list of large objects is released as soon as the tables are built. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
WebAssembly with -msse2 defines __SSE2__, but has no <cpuid.h> or x86 intrinsics headers. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Lookups in objects with 128 members or more now return the last member of a repeated key, like the linear search of smaller objects and like materialize() and basic_json::parse(). build_object_index let the first occurrence win, so the same text gave different results depending on the size of the object. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Lookups in objects with 128 members or more return the first member of a repeated key again, like the linear search of smaller objects. This undoes the code change of a695420; its test now expects the first member from lookups (with and without a table) and the last value from materialize() and basic_json::parse(). Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Silence bugprone-casting-through-void for the SSE loads (a reinterpret_cast would trip -Wcast-align=strict), use auto for a cast initialiser, add parentheses to a mixed expression, and name bugprone-std-namespace-modification in the NOLINTs of the tuple_size/tuple_element specialisations. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
…uilder.cpp Signed-off-by: Niels Lohmann <mail@nlohmann.me>
….cpp Signed-off-by: Niels Lohmann <mail@nlohmann.me>
…in32) Signed-off-by: Niels Lohmann <mail@nlohmann.me>
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 Signed-off-by: Niels Lohmann <mail@nlohmann.me>
…ump.cpp The dump and comparison tests moved to their own file in the dump pull request; the hash index tests of this branch join them there. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
The review of #5620 renamed document_data's tape/tape_size/tape_cap to nodes/node_count/node_capacity and made the decode arena a std::vector<char>; adapt the code of this change to it. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Qt defines slots as a macro, so a Qt header included before json_view.hpp broke the declarations of the local variable (see #5865). The test variable is renamed as well. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
From gregmarr's review of #5628: - Docs and comments say that the hash index is one of the object's keys. - object_index.hpp: conditional_static_cast for the hash, "slot" for the table position, no flag for a degenerate table, and a static_assert that the smallest indexed object is larger than the probe window. - scan.hpp unrolls with NLOHMANN_VIEW_REPEAT_FIRST_8/SECOND_8. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Both macros select between definitions of the same inline functions, so translation units that disagree on them violated the ODR. Like the other ABI-affecting macros, they now add a tag to the inline namespace (_vns and _vssse3), and the docs replace the "Define consistently" warning with the usual ABI compatibility note. From gregmarr's review of #5628. nlohmann_json.natvis covers twice as many tags now, so it lists four times as many namespaces (14.7 MB instead of 3.4 MB). Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
adaa127 to
586fab9
Compare
json.hpp, which object_index.hpp already includes, provides it; included directly, the json_view amalgamation (which only treats nlohmann/json.hpp as external) copied the json internals into json_view.hpp a second time, so the fuzzers, which include both single headers, failed with redefinitions (make_void, integer_sequence, priority_tag, ...). Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Scans string bytes with NEON/SSE2, validates non-ASCII text with simdjson's vector UTF-8 check, and adds a hash index for large objects to the view's parser; part of the zero-copy view stack for #5295, on top of #5624.
Summary
SIMD scanning
<cpuid.h>are only used on x86 targets, so other targets that define__SSE2__(such as Emscripten) use the portable code.std::atomic<int>initialized at compile time, so there is no guard of a local static and no global constructor (-Wglobal-constructors). The definitions do not depend on compiler flags, so translation units with different flags cannot violate the ODR.JSON_VIEW_USE_SSSE3only skips the CPU check; the same input is accepted either way.JSON_VIEW_NO_SIMDselects the portable code.open()stores the parent's frame field by field instead of building it on the stack and copying it. The copy was read back by loads wider than its stores, which waited until the stores retired (failed store forwarding). Apple M1 forwards such stores and does not change.targetattribute in modules, sosrc/modules/json.cppmdefinesNLOHMANN_VIEW_NO_TARGET_ATTRIBUTEthere; the check stays portable and the SSE2 code stays on.simd.hpp, in the README and inlicense.md.Object index
Lookups in objects are linear, as for
ordered_json. Objects with 128 members or more now get a hash table after parsing (detail/view/object_index.hpp):operator[],at(),find(),contains(),count(),value()and JSON pointers take constant time on average in such objects;extra;shrink_to_fit()trims the spare capacity of the index tables,memory_usage()counts them, and the list of noted objects is released after parsing;Docs, build files and other touched areas
JSON_VIEW_NO_SIMDandJSON_VIEW_USE_SSSE3; the architecture page describes the tables and whatextraholds for objects; the lookup pages and the "Duplicate keys" section note that the index leads to the first member.read()notes that reusing a document avoids the page faults of a fresh node index (about 40% of the parse time of a 55 MB document on x86-64 Linux).include/nlohmann/json_view.hpp,BUILD.bazel,src/modules/json.cppm,tests/CMakeLists.txt(the builder tests run again withJSON_VIEW_NO_SIMD, and on x86-64 withJSON_VIEW_USE_SSSE3 -mssse3),README.md,docs/mkdocs/docs/home/license.md,.gitignore(GCC module cache).Performance
Measured on Apple M1 Max (Apple clang -O2) and an x86-64 KVM VPS (GCC 13 / Clang 18 -O2, pinned to one core, about ±5-10% noise), µs, best of 3 interleaved rounds of 15 runs; files from nativejson-benchmark.
json_document::parse, previous PR → this PR:Object index: looking up every key of an object with 10,000 members takes 59.8 ms linearly and 0.16 ms with the index. Parsing stays within noise for documents without large objects.
The core library (
json::parse/dump) is unaffected: the changes are insideinclude/nlohmann/detail/view/andjson_view.hpp, plus the module file and build files; no file the core lexer or serializer uses is touched.Tests
json::acceptandjson::parse.JSON_VIEW_NO_SIMD, and on x86-64 withJSON_VIEW_USE_SSSE3 -mssse3.shrink_to_fit()releasing the tables' spare capacity; nested large objects, objects in arrays, and documents reused withread().Public API
No breaking changes. The view API is new in this stack, so nothing here changes a released interface.
JSON_VIEW_NO_SIMD(portable scanning) andJSON_VIEW_USE_SSSE3(skip the CPU check; without it, x86-64 builds use SSSE3 when the CPU has it, and the same inputs are accepted or rejected either way).JSON_VIEW_USE_SSSE3must be defined identically in every translation unit that includes the header.Written by Claude Code.
🤖 Generated with Claude Code