Compare commits

..
Author SHA1 Message Date
Niels Lohmann 16ed40b2d4 CBOR/MessagePack readers: leave one-element arrays to the general path
enter_cbor_array and enter_msgpack_array called the out-of-line scalar
run for every non-empty array. When the array's element is an object or
an array, as on every level of [[...]] or the single ring of a GeoJSON
polygon, the call reads nothing; it made 1,000 arrays nested in each
other cost 4 % more instructions (GCC 13) and up to 8 % more time (Apple
clang) than without the run. #5975 had fixed that with an inline
pre-check, which was later dropped after measuring canada only.

The run is now entered only for arrays of two or more elements: the same
comparison as before, so no other row changes. Arrays of one scalar,
which the run read faster, now take the general path; they are rare in
documents (one in citm_catalog, none in twitter or canada). The new
benchmark FromBinaryShape/singles measures them, next to
FromBinaryShape/nested.

Instructions per repetition, new/old, GCC 13 callgrind: nested arrays
0.973 (CBOR) and 0.971 (MessagePack), arrays of one scalar 1.037 and
1.021, every other row within 0.15 %.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-02 08:22:32 +01:00
9 changed files with 104 additions and 78 deletions

No files matched your search

@@ -406,7 +406,6 @@ using array_t = ArrayType<basic_json, AllocatorType<basic_json>>;
| `#!cpp operator<<(std::ostream&, const json_pointer&)` | streamability to `#!cpp std::ostream` | | `#!cpp operator<<(std::ostream&, const json_pointer&)` | streamability to `#!cpp std::ostream` |
| [`to_string`](../../api/basic_json/to_string.md) | conversion of `StringType` to `#!cpp std::string` (the function returns a `#!cpp std::string`) | | [`to_string`](../../api/basic_json/to_string.md) | conversion of `StringType` to `#!cpp std::string` (the function returns a `#!cpp std::string`) |
| exception messages | `data()` and `size()`, or `begin()` and `end()` | | exception messages | `data()` and `size()`, or `begin()` and `end()` |
| [`basic_json_view::dump`](../../api/basic_json_view/dump.md) | `reserve(n)` and `resize(n)` (one argument); `capacity()` is **not** required |
### Compatible types ### Compatible types
@@ -1856,14 +1856,18 @@ class binary_reader
Only the start of an array is tried, which covers arrays of numbers Only the start of an array is tried, which covers arrays of numbers
(e.g. coordinates) but costs arrays of other values only one call, not one (e.g. coordinates) but costs arrays of other values only one call, not one
per element. per element. An array of one element is not tried at all: that element is
mostly an object or an array (`[[...]]`, the single ring of a GeoJSON
polygon), for which the call would find no scalar, while a single scalar,
which the run would read faster, is rare. The benchmarks
`FromBinaryShape/nested` and `FromBinaryShape/singles` show both sides.
@param[in] len the number of elements @param[in] len the number of elements
@return whether the SAX parser accepted the array and the values read @return whether the SAX parser accepted the array and the values read
*/ */
bool enter_cbor_array(const std::size_t len) bool enter_cbor_array(const std::size_t len)
{ {
return enter_array(len) && (len == 0 || parse_cbor_scalar_run(std::integral_constant<bool, bulk_scan> {})); return enter_array(len) && (len <= 1 || parse_cbor_scalar_run(std::integral_constant<bool, bulk_scan> {}));
} }
/// input that is not contiguous: every value is read by parse_cbor_value /// input that is not contiguous: every value is read by parse_cbor_value
@@ -2764,12 +2768,16 @@ class binary_reader
/*! /*!
@brief open a MessagePack array and read the scalars it begins with (see @brief open a MessagePack array and read the scalars it begins with (see
@ref parse_msgpack_scalar_run) @ref parse_msgpack_scalar_run)
An array of one element is not tried, for the reason given at
@ref enter_cbor_array.
@param[in] len the number of elements @param[in] len the number of elements
@return whether the SAX parser accepted the array and the values read @return whether the SAX parser accepted the array and the values read
*/ */
bool enter_msgpack_array(const std::size_t len) bool enter_msgpack_array(const std::size_t len)
{ {
return enter_array(len) && (len == 0 || parse_msgpack_scalar_run(std::integral_constant<bool, bulk_scan> {})); return enter_array(len) && (len <= 1 || parse_msgpack_scalar_run(std::integral_constant<bool, bulk_scan> {}));
} }
/// input that is not contiguous: every value is read by parse_msgpack_value /// input that is not contiguous: every value is read by parse_msgpack_value
+4 -9
View File
@@ -47,7 +47,6 @@ class output_buffer
: m_out(sized(out, estimate)) : m_out(sized(out, estimate))
, m_pos(&m_out[0]) , m_pos(&m_out[0])
, m_end(m_pos + m_out.size()) , m_end(m_pos + m_out.size())
, m_reserved((std::max)(estimate, m_out.size()))
{} {}
void finish() void finish()
@@ -117,12 +116,11 @@ class output_buffer
const auto used = static_cast<std::size_t>(m_pos - m_out.data()); const auto used = static_cast<std::size_t>(m_pos - m_out.data());
// (a step does not go beyond the reserved estimate, so that a good // (a step does not go beyond the reserved estimate, so that a good
// estimate is never copied to a larger allocation; not a clamp: the // estimate is never copied to a larger allocation; not a clamp: the
// lower bound used + n + 256 can exceed m_reserved, and then it must win) // lower bound used + n + 256 can exceed capacity(), and then it must win)
const std::size_t size = (std::max)((std::min)(m_out.size() + step, m_reserved), used + n + 256); const std::size_t size = (std::max)((std::min)(m_out.size() + step, m_out.capacity()), used + n + 256);
if (size > m_reserved) if (size > m_out.capacity())
{ {
m_reserved = (std::max)(m_reserved * 2, size); m_out.reserve((std::max)(m_out.capacity() * 2, size));
m_out.reserve(m_reserved);
} }
m_out.resize(size); m_out.resize(size);
m_pos = &m_out[0] + used; m_pos = &m_out[0] + used;
@@ -132,9 +130,6 @@ class output_buffer
StringType& m_out; StringType& m_out;
char* m_pos; char* m_pos;
char* m_end; char* m_end;
/// what was last passed to reserve(), a lower bound of the capacity
/// (StringType need not have a capacity() member)
std::size_t m_reserved;
}; };
/// The length of the run at s that dump() writes unchanged without /// The length of the run at s that dump() writes unchanged without
+11 -3
View File
@@ -19863,14 +19863,18 @@ class binary_reader
Only the start of an array is tried, which covers arrays of numbers Only the start of an array is tried, which covers arrays of numbers
(e.g. coordinates) but costs arrays of other values only one call, not one (e.g. coordinates) but costs arrays of other values only one call, not one
per element. per element. An array of one element is not tried at all: that element is
mostly an object or an array (`[[...]]`, the single ring of a GeoJSON
polygon), for which the call would find no scalar, while a single scalar,
which the run would read faster, is rare. The benchmarks
`FromBinaryShape/nested` and `FromBinaryShape/singles` show both sides.
@param[in] len the number of elements @param[in] len the number of elements
@return whether the SAX parser accepted the array and the values read @return whether the SAX parser accepted the array and the values read
*/ */
bool enter_cbor_array(const std::size_t len) bool enter_cbor_array(const std::size_t len)
{ {
return enter_array(len) && (len == 0 || parse_cbor_scalar_run(std::integral_constant<bool, bulk_scan> {})); return enter_array(len) && (len <= 1 || parse_cbor_scalar_run(std::integral_constant<bool, bulk_scan> {}));
} }
/// input that is not contiguous: every value is read by parse_cbor_value /// input that is not contiguous: every value is read by parse_cbor_value
@@ -20771,12 +20775,16 @@ class binary_reader
/*! /*!
@brief open a MessagePack array and read the scalars it begins with (see @brief open a MessagePack array and read the scalars it begins with (see
@ref parse_msgpack_scalar_run) @ref parse_msgpack_scalar_run)
An array of one element is not tried, for the reason given at
@ref enter_cbor_array.
@param[in] len the number of elements @param[in] len the number of elements
@return whether the SAX parser accepted the array and the values read @return whether the SAX parser accepted the array and the values read
*/ */
bool enter_msgpack_array(const std::size_t len) bool enter_msgpack_array(const std::size_t len)
{ {
return enter_array(len) && (len == 0 || parse_msgpack_scalar_run(std::integral_constant<bool, bulk_scan> {})); return enter_array(len) && (len <= 1 || parse_msgpack_scalar_run(std::integral_constant<bool, bulk_scan> {}));
} }
/// input that is not contiguous: every value is read by parse_msgpack_value /// input that is not contiguous: every value is read by parse_msgpack_value
+4 -9
View File
@@ -8511,7 +8511,6 @@ class output_buffer
: m_out(sized(out, estimate)) : m_out(sized(out, estimate))
, m_pos(&m_out[0]) , m_pos(&m_out[0])
, m_end(m_pos + m_out.size()) , m_end(m_pos + m_out.size())
, m_reserved((std::max)(estimate, m_out.size()))
{} {}
void finish() void finish()
@@ -8581,12 +8580,11 @@ class output_buffer
const auto used = static_cast<std::size_t>(m_pos - m_out.data()); const auto used = static_cast<std::size_t>(m_pos - m_out.data());
// (a step does not go beyond the reserved estimate, so that a good // (a step does not go beyond the reserved estimate, so that a good
// estimate is never copied to a larger allocation; not a clamp: the // estimate is never copied to a larger allocation; not a clamp: the
// lower bound used + n + 256 can exceed m_reserved, and then it must win) // lower bound used + n + 256 can exceed capacity(), and then it must win)
const std::size_t size = (std::max)((std::min)(m_out.size() + step, m_reserved), used + n + 256); const std::size_t size = (std::max)((std::min)(m_out.size() + step, m_out.capacity()), used + n + 256);
if (size > m_reserved) if (size > m_out.capacity())
{ {
m_reserved = (std::max)(m_reserved * 2, size); m_out.reserve((std::max)(m_out.capacity() * 2, size));
m_out.reserve(m_reserved);
} }
m_out.resize(size); m_out.resize(size);
m_pos = &m_out[0] + used; m_pos = &m_out[0] + used;
@@ -8596,9 +8594,6 @@ class output_buffer
StringType& m_out; StringType& m_out;
char* m_pos; char* m_pos;
char* m_end; char* m_end;
/// what was last passed to reserve(), a lower bound of the capacity
/// (StringType need not have a capacity() member)
std::size_t m_reserved;
}; };
/// The length of the run at s that dump() writes unchanged without /// The length of the run at s that dump() writes unchanged without
+18
View File
@@ -629,6 +629,22 @@ static json make_containers()
return j; return j;
} }
/// many arrays of one number each: one-element arrays are left to the general
/// path (CBOR and MessagePack), which reads a lone number slower than the loop
/// for arrays of scalars would
static json make_singles()
{
json data = json::array();
for (std::size_t i = 0; i < 100000; ++i)
{
data.push_back(json::array({i}));
}
json j = json::object();
j["data"] = std::move(data);
return j;
}
/// one flat array of numbers: the scalar decoding path, which must not move /// one flat array of numbers: the scalar decoding path, which must not move
static json make_scalars() static json make_scalars()
{ {
@@ -676,6 +692,8 @@ BENCHMARK_CAPTURE(FromBinaryShape, containers / ubjson_optimized, make_container
BENCHMARK_CAPTURE(FromBinaryShape, containers / bjdata, make_containers, binary_format::bjdata); BENCHMARK_CAPTURE(FromBinaryShape, containers / bjdata, make_containers, binary_format::bjdata);
BENCHMARK_CAPTURE(FromBinaryShape, containers / bson, make_containers, binary_format::bson); BENCHMARK_CAPTURE(FromBinaryShape, containers / bson, make_containers, binary_format::bson);
BENCHMARK_CAPTURE(FromBinaryShape, containers / bon8, make_containers, binary_format::bon8); BENCHMARK_CAPTURE(FromBinaryShape, containers / bon8, make_containers, binary_format::bon8);
BENCHMARK_CAPTURE(FromBinaryShape, singles / cbor, make_singles, binary_format::cbor);
BENCHMARK_CAPTURE(FromBinaryShape, singles / msgpack, make_singles, binary_format::msgpack);
// BSON names every array element, so a large array measures key generation // BSON names every array element, so a large array measures key generation
// rather than scalar decoding and is left out here // rather than scalar decoding and is left out here
BENCHMARK_CAPTURE(FromBinaryShape, scalars / cbor, make_scalars, binary_format::cbor); BENCHMARK_CAPTURE(FromBinaryShape, scalars / cbor, make_scalars, binary_format::cbor);
-53
View File
@@ -10,7 +10,6 @@
#include "doctest_compatibility.h" #include "doctest_compatibility.h"
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <nlohmann/json_view.hpp>
#include <cstdint> #include <cstdint>
#include <string> #include <string>
@@ -484,56 +483,4 @@ TEST_CASE("alternative string type")
} }
} }
TEST_CASE("alternative string type with json_view")
{
// regression test for #5994: basic_json_view::dump() called
// StringType::capacity(), which alt_string (and the documented StringType
// requirements) do not provide
using alt_document = nlohmann::basic_json_document<alt_json, nlohmann::view_mode::read_only>;
using alt_editable_document = nlohmann::basic_json_document<alt_json, nlohmann::view_mode::editable>;
const char* text = R"({"a":[1,"x\u00e4"],"b":{"c":true,"d":null},"e":2.5})";
const alt_json j = alt_json::parse(text);
SECTION("read-only document")
{
const alt_document d = alt_document::parse(text);
CHECK(d.root().dump() == j.dump());
CHECK(d.root().dump(2) == j.dump(2));
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
CHECK(d.root().materialize() == j);
CHECK(d.root().contains(alt_string("b")));
CHECK(d.root()[alt_string("b")][alt_string("c")].dump() == "true");
}
SECTION("long output")
{
// more than one growth step of the output buffer
alt_json big = alt_json::array();
for (int i = 0; i < 20000; ++i)
{
big.push_back(alt_json::parse(text));
}
const alt_string big_text = big.dump();
const alt_document d = alt_document::parse(big_text);
CHECK(d.root().dump() == big_text);
CHECK(d.root().dump(4) == big.dump(4));
}
SECTION("editable document")
{
alt_editable_document d = alt_editable_document::parse(text);
d.set(d.root(), "f", "new");
d.push_back(d.root()[alt_string("a")], 3);
alt_json expected = j;
expected["f"] = "new";
expected["a"].push_back(3);
CHECK(d.root().materialize() == expected);
CHECK(alt_json::parse(d.root().dump()) == expected);
const alt_editable_document loaded = alt_editable_document::load(d.save());
CHECK(loaded.root().materialize() == expected);
}
}
DOCTEST_CLANG_SUPPRESS_WARNING_POP DOCTEST_CLANG_SUPPRESS_WARNING_POP
+28
View File
@@ -2520,6 +2520,34 @@ TEST_CASE("CBOR arrays of scalars from contiguous and non-contiguous input")
} }
} }
TEST_CASE("CBOR arrays of one element from contiguous and non-contiguous input")
{
// contiguous input leaves an array of one element to the general path
// rather than to the loop that reads arrays of scalars; both inputs must
// still give the same events, value, or error for the complete document
// and for every truncation of it, with enough bytes after each array that
// the loop could have been entered
const json j = json::parse(R"({"a": [[[[1.5]]]], "b": [7], "c": [{"x": 2.5}], "d": ["s"], "e": [[1, 2]], "f": [null], "g": 1.25})");
const std::vector<std::uint8_t> bytes = json::to_cbor(j);
CHECK(json::from_cbor(bytes) == j);
for (std::size_t length = 0; length <= bytes.size(); ++length)
{
CAPTURE(length)
const std::vector<std::uint8_t> contiguous(bytes.begin(), bytes.begin() + static_cast<std::ptrdiff_t>(length));
const std::list<std::uint8_t> bytewise(contiguous.begin(), contiguous.end());
CHECK(json::from_cbor(contiguous, true, false).dump() == json::from_cbor(bytewise.begin(), bytewise.end(), true, false).dump());
SaxEventLogger from_contiguous;
SaxEventLogger from_bytewise;
CHECK(json::sax_parse(contiguous, &from_contiguous, json::input_format_t::cbor, true, false)
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::cbor, true, false));
CHECK(from_contiguous.events == from_bytewise.events);
}
}
TEST_CASE("CBOR indefinite-length strings do not recurse per chunk") TEST_CASE("CBOR indefinite-length strings do not recurse per chunk")
{ {
// Reading an indefinite-length string or byte array used to call itself // Reading an indefinite-length string or byte array used to call itself
+28
View File
@@ -2311,3 +2311,31 @@ TEST_CASE("MessagePack arrays of scalars from contiguous and non-contiguous inpu
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::msgpack)); == json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::msgpack));
} }
} }
TEST_CASE("MessagePack arrays of one element from contiguous and non-contiguous input")
{
// contiguous input leaves an array of one element to the general path
// rather than to the loop that reads arrays of scalars; both inputs must
// still give the same events, value, or error for the complete document
// and for every truncation of it, with enough bytes after each array that
// the loop could have been entered
const json j = json::parse(R"({"a": [[[[1.5]]]], "b": [7], "c": [{"x": 2.5}], "d": ["s"], "e": [[1, 2]], "f": [null], "g": 1.25})");
const std::vector<std::uint8_t> bytes = json::to_msgpack(j);
CHECK(json::from_msgpack(bytes) == j);
for (std::size_t length = 0; length <= bytes.size(); ++length)
{
CAPTURE(length)
const std::vector<std::uint8_t> contiguous(bytes.begin(), bytes.begin() + static_cast<std::ptrdiff_t>(length));
const std::list<std::uint8_t> bytewise(contiguous.begin(), contiguous.end());
CHECK(json::from_msgpack(contiguous, true, false).dump() == json::from_msgpack(bytewise.begin(), bytewise.end(), true, false).dump());
SaxEventLogger from_contiguous;
SaxEventLogger from_bytewise;
CHECK(json::sax_parse(contiguous, &from_contiguous, json::input_format_t::msgpack, true, false)
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::msgpack, true, false));
CHECK(from_contiguous.events == from_bytewise.events);
}
}