Compare commits

...
Author SHA1 Message Date
Niels Lohmann ff8ba3cb74 Keep the serializer's buffers out of -Weffc++ and cpplint warnings
GCC's -Weffc++ asks for the three buffers in the member initialization
list, which would zero them again. They are now of a type with an empty
user-provided constructor (as the json_view builder's frame stack), so
they are listed there and stay uninitialized.

The 8-to-31-byte branch of the short-string writer no longer spans an
#if with a dangling else, which cpplint read as an else body without
braces.

dump() instructions (Apple clang, twitter/citm/canada, compact and
indented) are unchanged within noise (-0.3 % to +0.6 %).

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 10:51:40 +02:00
Niels Lohmann 0c3e83325a Test strings of 8 to 15 characters as one vector
The words of a string of 8 to 15 printable ASCII characters were each tested
with the word-at-a-time arithmetic. With SSE2 or NEON, the two overlapping
words are joined into one vector, which takes fewer operations to test; the
stores are unchanged. Targets without either keep the word arithmetic.

Apple M1, Apple clang, retired instructions per dump(), on top of the
previous changes:
  jeopardy 451M -> 430M (-5 %)   citm_catalog 5.60M -> 5.37M (-4 %)
  twitter 3.31M -> 3.25M (-2 %)  keys and values of 1..40 characters -1 %
Linux x86-64 (callgrind), gcc 13 / clang 18: jeopardy -15 % / -7 %,
citm_catalog -6 % / -5 %, twitter -6 % / -4 %.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:31 +02:00
Niels Lohmann ebcd4ae4ab Copy the short runs between escapes as a block of 16 bytes
The characters between two escapes (a quote, a backslash, a control
character) are copied to the write buffer by memcpy with a length known only
at run time. For a run of up to 16 characters that has 16 or more bytes in
the string behind its start and in the buffer behind its end, a copy of a
fixed block of 16 bytes does the same without the call; the surplus bytes in
the buffer are overwritten by what follows.

Apple M1, Apple clang, retired instructions per dump(), on top of the
previous changes:
  JSON in JSON 8.32M -> 7.52M (-10 %)   paths 8.17M -> 7.49M (-8 %)
  log lines 7.49M -> 7.14M (-5 %)       cjk, emoji text (no escapes): +-1 %
Linux x86-64 (callgrind), gcc 13 / clang 18:
  JSON in JSON -11 % / -10 %, paths -7 % / -6 %, log lines -5 % / -4 %.
The branch is marked unlikely: text without escapes (CJK, accented Latin)
has few short runs, and without the hint it paid up to 5 % for the check.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:28 +02:00
Niels Lohmann 47462527b2 Write a line break and the indentation of the next line together
A pretty-printed array or object wrote ",\n", checked the room for the
indentation, and stored it, for every element. The line break and the
usual indentation (up to 32 characters, with room in the write buffer) are
now written with one check and one run of stores, also after the opening
bracket and in front of the closing one. Other indentations take the
unchanged path.

Apple M1, Apple clang, retired instructions per dump(4), on top of the
previous changes:
  citm_catalog 7.28M -> 7.06M (-3 %)   canada 44.4M -> 43.3M (-3 %)
  twitter 3.91M -> 3.78M (-3 %)        jeopardy -1 %
Linux x86-64 (callgrind), gcc 13 / clang 18, dump(4) / dump(1, '\t'):
  citm_catalog dump(4) -3 % / -4 %, tab -4 % / -5 %, with the function
  inlined (without that GCC wrote the combined step out of line and was
  5 % slower than before the change).
Compact output is unchanged.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:26 +02:00
Niels Lohmann ae30ba55c3 Write strings and keys of up to 32 characters in one go with vectors
The previous change handles plain strings of up to 16 characters with
two overlapping 8-byte words. With SSE2 or NEON, two overlapping 16-byte
vectors do the same for 16 to 32 characters: one test for a quote,
backslash, control character, DEL or non-ASCII byte over both, and two
stores. Targets without either keep the limit of 16.

Apple M1, Apple clang, retired instructions per dump(), on top of the
previous changes (it replaces the table of #5878 for these strings):
  keys and values of 1..40 characters 7.07M -> 4.94M (-30 %)
  twitter 4.15M -> 3.30M (-21 %)   paths 9.09M -> 8.08M (-11 %)
  citm_catalog 5.76M -> 5.53M (-4 %)   jeopardy 461M -> 451M (-2 %)
Linux x86-64 (callgrind), gcc 13 / clang 18: twitter -11 % / -9 %,
keys and values of 1..40 characters -19 % / -17 %, paths -7 % / -7 %.
Documents without strings in that range are unchanged.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:23 +02:00
Niels Lohmann 731363a325 Write the usual indentation with four stores instead of memset
Every line of a pretty-printed document starts with its indentation, which
was written by calling memset with a length known only at run time.

An indentation of up to 32 characters, with room in the write buffer,
stores four words of the indentation character and claims the part it
needs. Wider indentations and a nearly full buffer take the unchanged
path.

Apple M1, Apple clang, retired instructions per dump(4), on top of the
previous changes:
  canada  50.7M -> 44.4M (-13 %)   citm_catalog 8.85M -> 7.48M (-16 %)
  twitter 5.13M -> 4.71M (-8 %)    jeopardy 564M -> 518M (-8 %)
  a 139-byte object 3277 -> 2924 (-11 %)
Linux x86-64 (callgrind), the whole series against develop, gcc 13 / clang 18:
  canada, indent 4: -13 % / -14 %.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:21 +02:00
Niels Lohmann fdcf3aa390 Do not zero the serializer's buffers
Constructing a serializer value-initialized its three buffers (64 + 512
+ 1024 bytes). Each is written before it is read and only the part that
was written is ever read, so the zeroing was never needed, but it took
about 170 instructions per dump(), more than half of the cost of dumping
a small value.

Apple M1, Apple clang, per dump():
  null                325 -> 153 instructions
  [1,2,3]             629 -> 458 instructions
  small object (134 B) 2795 -> 2624 instructions, 190 -> 165 ns (-13 %)
  a 139-byte object, compact: 2763 -> 2596 instructions (-6 %)
Large documents are unchanged. Checked with valgrind memcheck over the
differential corpus (no use of uninitialized values), and with GCC 13
-Wall -Wextra -Wmaybe-uninitialized at -O0 to -O3 and -Os.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:18 +02:00
Niels Lohmann 209348c151 Escape runs of multi-byte characters straight into the write buffer
With ensure_ascii, every non-ASCII character went through the UTF-8
state machine one byte at a time and its \uXXXX escape was assembled in
string_buffer and copied to the write buffer afterwards.

A run of well-formed multi-byte characters is now decoded and escaped
directly into the write buffer. The first byte that is ASCII or starts an
ill-formed or truncated sequence ends the run and is left to the
unchanged byte-at-a-time path, so error handling and its messages
(positions, replacement, ignore, keep) are as before.

Apple M1, Apple clang, retired instructions per dump(-1, ' ', true):
  cjk text      15.9M -> 6.94M (-56 %)   twitter 9.10M -> 6.16M (-32 %)
  emoji text    8.89M -> 6.08M (-32 %)   latin text 10.3M -> 9.23M (-10 %)
Pure ASCII documents with escapes (logs, JSON in JSON) and all modes
without ensure_ascii are unchanged.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:16 +02:00
Niels Lohmann 7c3888fe42 Write the scalar elements of a container without entering dump_internal()
Every element of an array or object went through dump_internal(), whose
frame holds the state for descending into a container (callee-saved
registers, indentation, depth). A scalar, which is what most elements
are, needs none of it, and paid for the entry and exit regardless.

The loops now call dump_scalar() directly for anything that is not an
object or an array, and dump_internal() for those only.

Apple M1, Apple clang, retired instructions per dump(), on top of the
previous change:
  small object 2997 -> 2712 (-10 %)   signed_ints 276M -> 252M (-9 %)
  floats 323M -> 301M (-7 %)          jeopardy 501M -> 462M (-8 %)
  citm_catalog 5.96M -> 5.61M (-6 %)  twitter 4.40M -> 4.12M (-6 %)
  canada 39.2M -> 37.0M (-5 %)

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:13 +02:00
Niels Lohmann 5bbc7a1098 Write short plain strings and keys in one go in dump()
Most strings in real documents are object keys and short values. For
those, dump_escaped() scanned the bytes, called memcpy with a length
known only at run time, and the surrounding quote, colon and space were
three more bounds-checked writes.

A string of up to 16 printable ASCII characters (no quote, backslash or
DEL) is now read with two overlapping loads, checked with one SWAR test
per word, and written together with its quotes and the colon with two
overlapping stores. Anything else, and a write buffer without room, takes
the general path unchanged. Little-endian targets only; others keep the
general path.

dump_scalar() held the code of every scalar type and saved and restored
all callee-saved registers on every call. It only dispatches now, and the
string, integer, float and binary writers are functions of their own, so
each pays only for what it uses (the string path keeps the check for
long strings in front of any attempt, which keeps documents of long
strings neutral).

Apple M1, Apple clang, retired instructions per dump(), on top of the
printable-ASCII table of #5878:
  citm_catalog  7.54M -> 5.96M (-21 %)   twitter 4.87M -> 4.40M (-10 %)
  jeopardy      603M -> 501M   (-17 %)   logs     8.65M -> 8.17M (-5 %)
  a 139-byte object 3497 -> 2997 (-14 %)   signed_ints -4 %, canada -3 %
Linux x86-64 (callgrind), the whole series against develop, gcc 13 / clang 18:
  citm_catalog -16 % / -19 % (compact), twitter -22 % / -24 %.
Documents of long strings (paths, latin and CJK text) are within 1 %.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:11 +02:00
4 changed files with 1193 additions and 228 deletions

No files matched your search

@@ -33,6 +33,7 @@
#undef JSON_SCAN_SSSE3_TARGET #undef JSON_SCAN_SSSE3_TARGET
#undef JSON_SCAN_VECTOR #undef JSON_SCAN_VECTOR
#undef JSON_SCAN_VECTOR_UTF8 #undef JSON_SCAN_VECTOR_UTF8
#undef JSON_DUMP_SHORT_STRINGS
#ifndef JSON_TEST_KEEP_MACROS #ifndef JSON_TEST_KEEP_MACROS
#undef JSON_CATCH #undef JSON_CATCH
+550 -114
View File
@@ -34,6 +34,14 @@
#include <nlohmann/detail/string_utils.hpp> #include <nlohmann/detail/string_utils.hpp>
#include <nlohmann/detail/value_t.hpp> #include <nlohmann/detail/value_t.hpp>
// the short-string fast path of the serializer assembles words from little-endian
// loads, see serializer::put_short_quoted()
#if defined(_MSC_VER) || defined(__x86_64__) || defined(__i386__) || (defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
#define JSON_DUMP_SHORT_STRINGS 1
#else
#define JSON_DUMP_SHORT_STRINGS 0
#endif
NLOHMANN_JSON_NAMESPACE_BEGIN NLOHMANN_JSON_NAMESPACE_BEGIN
namespace detail namespace detail
{ {
@@ -80,11 +88,14 @@ class serializer
const std::size_t indent_step_ = 0, const std::size_t indent_step_ = 0,
error_handler_t error_handler_ = error_handler_t::strict) error_handler_t error_handler_ = error_handler_t::strict)
: o(&s) : o(&s)
, number_buffer() // NOLINT(readability-redundant-member-init)
, string_buffer() // NOLINT(readability-redundant-member-init)
, indent_char(ichar) , indent_char(ichar)
, pretty_print(pretty_print_) , pretty_print(pretty_print_)
, ensure_ascii(ensure_ascii_) , ensure_ascii(ensure_ascii_)
, indent_step(indent_step_) , indent_step(indent_step_)
, error_handler(error_handler_) , error_handler(error_handler_)
, write_buffer() // NOLINT(readability-redundant-member-init)
{} {}
// deleted because of pointer members // deleted because of pointer members
@@ -159,34 +170,27 @@ class serializer
if (pretty_print) if (pretty_print)
{ {
put_literal("{\n");
// variable to hold indentation for recursive calls // variable to hold indentation for recursive calls
const auto new_indent = next_indent(current_indent, indent_step); const auto new_indent = next_indent(current_indent, indent_step);
put_literal_indent("{\n", new_indent);
// first n-1 elements // first n-1 elements
auto i = val.m_data.m_value.object->cbegin(); auto i = val.m_data.m_value.object->cbegin();
for (std::size_t cnt = 0; cnt < val.m_data.m_value.object->size() - 1; ++cnt, ++i) for (std::size_t cnt = 0; cnt < val.m_data.m_value.object->size() - 1; ++cnt, ++i)
{ {
put_indent(new_indent); put_quoted(i->first, "\": ");
put_char('"'); dump_element(i->second, new_indent, depth + 1);
dump_escaped(i->first); put_literal_indent(",\n", new_indent);
put_literal("\": ");
dump_internal(i->second, new_indent, depth + 1);
put_literal(",\n");
} }
// last element // last element
JSON_ASSERT(i != val.m_data.m_value.object->cend()); JSON_ASSERT(i != val.m_data.m_value.object->cend());
JSON_ASSERT(std::next(i) == val.m_data.m_value.object->cend()); JSON_ASSERT(std::next(i) == val.m_data.m_value.object->cend());
put_indent(new_indent); put_quoted(i->first, "\": ");
put_char('"'); dump_element(i->second, new_indent, depth + 1);
dump_escaped(i->first);
put_literal("\": ");
dump_internal(i->second, new_indent, depth + 1);
put_char('\n'); put_literal_indent("\n", current_indent);
put_indent(current_indent);
put_char('}'); put_char('}');
} }
else else
@@ -197,20 +201,16 @@ class serializer
auto i = val.m_data.m_value.object->cbegin(); auto i = val.m_data.m_value.object->cbegin();
for (std::size_t cnt = 0; cnt < val.m_data.m_value.object->size() - 1; ++cnt, ++i) for (std::size_t cnt = 0; cnt < val.m_data.m_value.object->size() - 1; ++cnt, ++i)
{ {
put_char('"'); put_quoted(i->first, "\":");
dump_escaped(i->first); dump_element(i->second, current_indent, depth + 1);
put_literal("\":");
dump_internal(i->second, current_indent, depth + 1);
put_char(','); put_char(',');
} }
// last element // last element
JSON_ASSERT(i != val.m_data.m_value.object->cend()); JSON_ASSERT(i != val.m_data.m_value.object->cend());
JSON_ASSERT(std::next(i) == val.m_data.m_value.object->cend()); JSON_ASSERT(std::next(i) == val.m_data.m_value.object->cend());
put_char('"'); put_quoted(i->first, "\":");
dump_escaped(i->first); dump_element(i->second, current_indent, depth + 1);
put_literal("\":");
dump_internal(i->second, current_indent, depth + 1);
put_char('}'); put_char('}');
} }
@@ -234,27 +234,24 @@ class serializer
if (pretty_print) if (pretty_print)
{ {
put_literal("[\n");
// variable to hold indentation for recursive calls // variable to hold indentation for recursive calls
const auto new_indent = next_indent(current_indent, indent_step); const auto new_indent = next_indent(current_indent, indent_step);
put_literal_indent("[\n", new_indent);
// first n-1 elements // first n-1 elements
for (auto i = val.m_data.m_value.array->cbegin(); for (auto i = val.m_data.m_value.array->cbegin();
i != val.m_data.m_value.array->cend() - 1; ++i) i != val.m_data.m_value.array->cend() - 1; ++i)
{ {
put_indent(new_indent); dump_element(*i, new_indent, depth + 1);
dump_internal(*i, new_indent, depth + 1); put_literal_indent(",\n", new_indent);
put_literal(",\n");
} }
// last element // last element
JSON_ASSERT(!val.m_data.m_value.array->empty()); JSON_ASSERT(!val.m_data.m_value.array->empty());
put_indent(new_indent); dump_element(val.m_data.m_value.array->back(), new_indent, depth + 1);
dump_internal(val.m_data.m_value.array->back(), new_indent, depth + 1);
put_char('\n'); put_literal_indent("\n", current_indent);
put_indent(current_indent);
put_char(']'); put_char(']');
} }
else else
@@ -265,13 +262,13 @@ class serializer
for (auto i = val.m_data.m_value.array->cbegin(); for (auto i = val.m_data.m_value.array->cbegin();
i != val.m_data.m_value.array->cend() - 1; ++i) i != val.m_data.m_value.array->cend() - 1; ++i)
{ {
dump_internal(*i, current_indent, depth + 1); dump_element(*i, current_indent, depth + 1);
put_char(','); put_char(',');
} }
// last element // last element
JSON_ASSERT(!val.m_data.m_value.array->empty()); JSON_ASSERT(!val.m_data.m_value.array->empty());
dump_internal(val.m_data.m_value.array->back(), current_indent, depth + 1); dump_element(val.m_data.m_value.array->back(), current_indent, depth + 1);
put_char(']'); put_char(']');
} }
@@ -294,6 +291,27 @@ class serializer
} }
private: private:
/*!
@brief serialize an element of a container
Most elements are scalars, which need none of the state dump_internal() keeps
for a container; writing them from here spares each the entry and exit of
that function.
*/
JSON_HEDLEY_ALWAYS_INLINE void dump_element(const BasicJsonType& val,
const std::size_t current_indent,
const std::size_t depth)
{
if (val.m_data.m_type == value_t::object || val.m_data.m_type == value_t::array)
{
dump_internal(val, current_indent, depth);
}
else
{
dump_scalar(val, current_indent);
}
}
/*! /*!
@brief write out @a val and everything below it without the call stack @brief write out @a val and everything below it without the call stack
@@ -353,16 +371,13 @@ class serializer
put_indent(frame.child_indent); put_indent(frame.child_indent);
} }
put_char('"');
dump_escaped(frame.object_it->first);
if (pretty_print) if (pretty_print)
{ {
put_literal("\": "); put_quoted(frame.object_it->first, "\": ");
} }
else else
{ {
put_literal("\":"); put_quoted(frame.object_it->first, "\":");
} }
const BasicJsonType& element = frame.object_it->second; const BasicJsonType& element = frame.object_it->second;
@@ -537,78 +552,13 @@ class serializer
{ {
case value_t::string: case value_t::string:
{ {
put_char('"'); dump_string_value(*val.m_data.m_value.string);
dump_escaped(*val.m_data.m_value.string);
put_char('"');
return; return;
} }
case value_t::binary: case value_t::binary:
{ {
if (pretty_print) dump_binary_value(current_indent, *val.m_data.m_value.binary);
{
put_literal("{\n");
// variable to hold indentation for the bytes
const auto new_indent = next_indent(current_indent, indent_step);
put_indent(new_indent);
put_literal("\"bytes\": [");
if (!val.m_data.m_value.binary->empty())
{
for (auto i = val.m_data.m_value.binary->cbegin();
i != val.m_data.m_value.binary->cend() - 1; ++i)
{
dump_byte(*i);
put_literal(", ");
}
dump_byte(val.m_data.m_value.binary->back());
}
put_literal("],\n");
put_indent(new_indent);
put_literal("\"subtype\": ");
if (val.m_data.m_value.binary->has_subtype())
{
dump_integer(val.m_data.m_value.binary->subtype());
}
else
{
put_literal("null");
}
put_char('\n');
put_indent(current_indent);
put_char('}');
}
else
{
put_literal("{\"bytes\":[");
if (!val.m_data.m_value.binary->empty())
{
for (auto i = val.m_data.m_value.binary->cbegin();
i != val.m_data.m_value.binary->cend() - 1; ++i)
{
dump_byte(*i);
put_char(',');
}
dump_byte(val.m_data.m_value.binary->back());
}
put_literal("],\"subtype\":");
if (val.m_data.m_value.binary->has_subtype())
{
dump_integer(val.m_data.m_value.binary->subtype());
put_char('}');
}
else
{
put_literal("null}");
}
}
return; return;
} }
@@ -627,19 +577,19 @@ class serializer
case value_t::number_integer: case value_t::number_integer:
{ {
dump_integer(val.m_data.m_value.number_integer); dump_signed_value(val.m_data.m_value.number_integer);
return; return;
} }
case value_t::number_unsigned: case value_t::number_unsigned:
{ {
dump_integer(val.m_data.m_value.number_unsigned); dump_unsigned_value(val.m_data.m_value.number_unsigned);
return; return;
} }
case value_t::number_float: case value_t::number_float:
{ {
dump_float(val.m_data.m_value.number_float); dump_float_value(val.m_data.m_value.number_float);
return; return;
} }
@@ -662,6 +612,107 @@ class serializer
} }
} }
/// @brief write the string @a s in quotes; kept out of line, so that the
/// other scalars do not pay for its register needs (and the other way round)
JSON_HEDLEY_NEVER_INLINE void dump_string_value(const string_t& s)
{
#if JSON_DUMP_SHORT_STRINGS
if (s.size() <= short_string_max && put_short_quoted(s, "\""))
{
return;
}
#endif
put_char('"');
dump_escaped(s);
put_char('"');
}
// The numbers, and the rest of what dump_scalar() writes, are left to
// functions of their own as well: a single function holding all of them
// saves and restores every register any of them needs, on every call. Now
// dump_scalar() only dispatches, and each writer pays for what it uses.
JSON_HEDLEY_NEVER_INLINE void dump_signed_value(const number_integer_t x)
{
dump_integer(x);
}
JSON_HEDLEY_NEVER_INLINE void dump_unsigned_value(const number_unsigned_t x)
{
dump_integer(x);
}
JSON_HEDLEY_NEVER_INLINE void dump_float_value(const number_float_t x)
{
dump_float(x);
}
JSON_HEDLEY_NEVER_INLINE void dump_binary_value(const std::size_t current_indent, const typename BasicJsonType::binary_t& binary)
{
if (pretty_print)
{
put_literal("{\n");
// variable to hold indentation for the bytes
const auto new_indent = next_indent(current_indent, indent_step);
put_indent(new_indent);
put_literal("\"bytes\": [");
if (!binary.empty())
{
for (auto i = binary.cbegin();
i != binary.cend() - 1; ++i)
{
dump_byte(*i);
put_literal(", ");
}
dump_byte(binary.back());
}
put_literal("],\n");
put_indent(new_indent);
put_literal("\"subtype\": ");
if (binary.has_subtype())
{
dump_integer(binary.subtype());
}
else
{
put_literal("null");
}
put_char('\n');
put_indent(current_indent);
put_char('}');
}
else
{
put_literal("{\"bytes\":[");
if (!binary.empty())
{
for (auto i = binary.cbegin();
i != binary.cend() - 1; ++i)
{
dump_byte(*i);
put_char(',');
}
dump_byte(binary.back());
}
put_literal("],\"subtype\":");
if (binary.has_subtype())
{
dump_integer(binary.subtype());
put_char('}');
}
else
{
put_literal("null}");
}
}
}
/*! /*!
@brief the indentation level to use for the children of the current value @brief the indentation level to use for the children of the current value
@@ -678,6 +729,217 @@ class serializer
return new_indent; return new_indent;
} }
private:
/*!
@brief append the string @a s in quotes, followed by @a tail
The quote, the string and @a tail (the closing quote, possibly with the
colon of an object key) are the bulk of what most documents consist of, and
most keys and many values are short and plain. Those are handled in one go
by put_short_quoted(); everything else takes the general path.
*/
template<std::size_t N>
void put_quoted(const string_t& s, const char (&tail)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
{
#if JSON_DUMP_SHORT_STRINGS
if (put_short_quoted(s, tail))
{
return;
}
#endif
put_char('"');
dump_escaped(s);
put_literal(tail);
}
#if JSON_DUMP_SHORT_STRINGS
/// the longest string put_short_quoted() takes
static constexpr std::size_t short_string_max = JSON_SCAN_VECTOR ? 32 : 16;
/// the high bit of every byte of @a v that is not printable ASCII or is a
/// quote, a backslash, or DEL (zero if the bytes can be copied as they are)
static std::uint64_t swar_not_plain(const std::uint64_t v) noexcept
{
constexpr std::uint64_t ones = 0x0101010101010101ull;
const std::uint64_t q = v ^ 0x2222222222222222ull; // '"'
const std::uint64_t b = v ^ 0x5C5C5C5C5C5C5C5Cull; // '\\'
return (((q - ones) & ~q) | ((b - ones) & ~b) // == '"', == '\\'
| ((v - 0x2020202020202020ull) & ~v) // < 0x20
| v | (v + ones)) // >= 0x7F
& 0x8080808080808080ull;
}
template<typename WordType>
static WordType load_word(const char* p) noexcept
{
WordType w;
std::memcpy(&w, p, sizeof(w));
return w;
}
template<typename WordType>
static void store_word(char* p, const WordType w) noexcept
{
std::memcpy(p, &w, sizeof(w));
}
/*!
@brief write `"`, @a s and @a tail if @a s is short and needs no escaping
A string of up to 16 (32 with vector instructions) printable ASCII
characters (no quote, backslash, or DEL) is read with two overlapping loads
and written with two overlapping stores, instead of scanning it and calling
memcpy with a length that is only known at run time. Returns false, having
changed nothing, if @a s does not qualify or the write buffer lacks the
room.
*/
template<std::size_t N>
bool put_short_quoted(const string_t& s, const char (&tail)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
{
const std::size_t n = s.size();
if (n > short_string_max || write_buffer_pos + n + N > write_buffer.size())
{
return false;
}
char* d = write_buffer.data() + write_buffer_pos + 1;
const char* const p = s.data();
if (n >= 8)
{
#if JSON_SCAN_VECTOR
if (!(n >= 16 ? copy_plain_vectors(d, p, n) : copy_plain_halves(d, p, n)))
{
return false;
}
#else
const auto a = load_word<std::uint64_t>(p);
const auto b = load_word<std::uint64_t>(p + n - 8);
if ((swar_not_plain(a) | swar_not_plain(b)) != 0)
{
return false;
}
store_word(d, a);
store_word(d + n - 8, b);
#endif
}
else if (n >= 4)
{
const auto a = load_word<std::uint32_t>(p);
const auto b = load_word<std::uint32_t>(p + n - 4);
// the bytes above the string are filled with a plain character
const std::uint64_t w = a | (static_cast<std::uint64_t>(b) << (8 * (n - 4)))
| (0x4141414141414141ull << (8 * n));
if (swar_not_plain(w) != 0)
{
return false;
}
store_word(d, a);
store_word(d + n - 4, b);
}
else if (n >= 2)
{
const auto a = load_word<std::uint16_t>(p);
const auto b = load_word<std::uint16_t>(p + n - 2);
const std::uint64_t w = a | (static_cast<std::uint64_t>(b) << (8 * (n - 2)))
| (0x4141414141414141ull << (8 * n));
if (swar_not_plain(w) != 0)
{
return false;
}
store_word(d, a);
store_word(d + n - 2, b);
}
else if (n == 1)
{
const std::uint64_t w = static_cast<std::uint64_t>(static_cast<unsigned char>(p[0])) | (0x4141414141414141ull << 8u);
if (swar_not_plain(w) != 0)
{
return false;
}
*d = p[0];
}
d[-1] = '"';
std::memcpy(d + n, tail, N - 1);
write_buffer_pos += n + N;
return true;
}
#if JSON_SCAN_VECTOR
/// copy the 8 to 15 bytes [@a p, @a p + @a n) to @a d with two overlapping
/// words, tested together as one vector, see copy_plain_vectors()
static bool copy_plain_halves(char* d, const char* p, const std::size_t n) noexcept
{
#if JSON_SCAN_NEON
const uint8x8_t lo = vld1_u8(reinterpret_cast<const std::uint8_t*>(p));
const uint8x8_t hi = vld1_u8(reinterpret_cast<const std::uint8_t*>(p + n - 8));
const uint8x16_t in = vcombine_u8(lo, hi);
const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))),
vorrq_u8(vcltq_s8(vreinterpretq_s8_u8(in), vdupq_n_s8(0x20)), vceqq_u8(in, vdupq_n_u8(0x7F))));
if (vmaxvq_u8(special) != 0)
{
return false;
}
vst1_u8(reinterpret_cast<std::uint8_t*>(d), lo);
vst1_u8(reinterpret_cast<std::uint8_t*>(d + n - 8), hi);
#else
const __m128i lo = _mm_loadl_epi64(static_cast<const __m128i*>(static_cast<const void*>(p))); // NOLINT(bugprone-casting-through-void)
const __m128i hi = _mm_loadl_epi64(static_cast<const __m128i*>(static_cast<const void*>(p + n - 8))); // NOLINT(bugprone-casting-through-void)
const __m128i in = _mm_unpacklo_epi64(lo, hi);
const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))),
_mm_or_si128(_mm_cmplt_epi8(in, _mm_set1_epi8(0x20)), _mm_cmpeq_epi8(in, _mm_set1_epi8(0x7F))));
if (_mm_movemask_epi8(special) != 0)
{
return false;
}
_mm_storel_epi64(static_cast<__m128i*>(static_cast<void*>(d)), lo); // NOLINT(bugprone-casting-through-void)
_mm_storel_epi64(static_cast<__m128i*>(static_cast<void*>(d + n - 8)), hi); // NOLINT(bugprone-casting-through-void)
#endif
return true;
}
/// copy the 16 to 32 bytes [@a p, @a p + @a n) to @a d with two overlapping
/// vectors, unless one of them is a quote, a backslash, a control
/// character, DEL or not ASCII
static bool copy_plain_vectors(char* d, const char* p, const std::size_t n) noexcept
{
#if JSON_SCAN_NEON
const uint8x16_t a = vld1q_u8(reinterpret_cast<const std::uint8_t*>(p));
const uint8x16_t b = vld1q_u8(reinterpret_cast<const std::uint8_t*>(p + n - 16));
const uint8x16_t special = vorrq_u8(vorrq_u8(vorrq_u8(vceqq_u8(a, vdupq_n_u8('"')), vceqq_u8(a, vdupq_n_u8('\\'))),
vorrq_u8(vceqq_u8(b, vdupq_n_u8('"')), vceqq_u8(b, vdupq_n_u8('\\')))),
vorrq_u8(vorrq_u8(vcltq_s8(vreinterpretq_s8_u8(a), vdupq_n_s8(0x20)), vcltq_s8(vreinterpretq_s8_u8(b), vdupq_n_s8(0x20))),
vorrq_u8(vceqq_u8(a, vdupq_n_u8(0x7F)), vceqq_u8(b, vdupq_n_u8(0x7F)))));
if (vmaxvq_u8(special) != 0)
{
return false;
}
vst1q_u8(reinterpret_cast<std::uint8_t*>(d), a);
vst1q_u8(reinterpret_cast<std::uint8_t*>(d + n - 16), b);
#else
const __m128i a = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(p))); // NOLINT(bugprone-casting-through-void)
const __m128i b = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(p + n - 16))); // NOLINT(bugprone-casting-through-void)
const __m128i quote = _mm_set1_epi8('"');
const __m128i backslash = _mm_set1_epi8('\\');
const __m128i space = _mm_set1_epi8(0x20);
const __m128i del = _mm_set1_epi8(0x7F);
const __m128i special = _mm_or_si128(_mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(a, quote), _mm_cmpeq_epi8(a, backslash)),
_mm_or_si128(_mm_cmpeq_epi8(b, quote), _mm_cmpeq_epi8(b, backslash))),
_mm_or_si128(_mm_or_si128(_mm_cmplt_epi8(a, space), _mm_cmplt_epi8(b, space)),
_mm_or_si128(_mm_cmpeq_epi8(a, del), _mm_cmpeq_epi8(b, del))));
if (_mm_movemask_epi8(special) != 0)
{
return false;
}
_mm_storeu_si128(static_cast<__m128i*>(static_cast<void*>(d)), a); // NOLINT(bugprone-casting-through-void)
_mm_storeu_si128(static_cast<__m128i*>(static_cast<void*>(d + n - 16)), b); // NOLINT(bugprone-casting-through-void)
#endif
return true;
}
#endif
#endif
JSON_PRIVATE_UNLESS_TESTED: JSON_PRIVATE_UNLESS_TESTED:
/*! /*!
@brief dump escaped string @brief dump escaped string
@@ -690,14 +952,23 @@ class serializer
@param[in] s the string to escape @param[in] s the string to escape
Complexity: Linear in the length of string @a s. Complexity: Linear in the length of string @a s.
@note kept out of line: with the short-string attempts in front of it gone
from here, compilers would otherwise inline it into @ref put_quoted
and then no longer into the loops that call that
*/ */
void dump_escaped(const string_t& s) JSON_HEDLEY_NEVER_INLINE void dump_escaped(const string_t& s)
{ {
// Short strings of printable ASCII - most object keys and many values - // Short strings of printable ASCII - most object keys and many values -
// are checked byte by byte against a table and then copied in one go. // are checked byte by byte against a table and then copied in one go.
// For them, this is cheaper than starting the vector scanner below, // For them, this is cheaper than starting the vector scanner below,
// whose setup costs more than such a string takes to scan. The same // whose setup costs more than such a string takes to scan. The same
// check serves both values of ensure_ascii. // check serves both values of ensure_ascii.
//
// Where put_short_quoted() exists and takes strings of up to 32 bytes,
// the callers have tried it already, and repeating the attempt only
// costs time for strings that need escaping or are not ASCII.
#if !(JSON_DUMP_SHORT_STRINGS && JSON_SCAN_VECTOR)
if (s.size() <= 32) if (s.size() <= 32)
{ {
const auto& copyable = ascii_copyable(); const auto& copyable = ascii_copyable();
@@ -719,6 +990,7 @@ class serializer
return; return;
} }
} }
#endif
// dispatch once here rather than test the flag inside the loop: it does // dispatch once here rather than test the flag inside the loop: it does
// not change while a string is written, and folding it lets each of the // not change while a string is written, and folding it lets each of the
@@ -796,7 +1068,7 @@ class serializer
put_buffer(string_buffer, bytes); put_buffer(string_buffer, bytes);
bytes = 0; bytes = 0;
} }
put_string(s, i, i + run); put_run(s, i, run);
bytes_after_last_accept = 0; bytes_after_last_accept = 0;
undumped_chars = 0; undumped_chars = 0;
i += run; i += run;
@@ -836,6 +1108,27 @@ class serializer
const auto byte = static_cast<std::uint8_t>(s[i]); const auto byte = static_cast<std::uint8_t>(s[i]);
if (EnsureAscii && state == UTF8_ACCEPT && byte >= 0x80)
{
// a run of well-formed multi-byte characters is escaped
// straight into the write buffer; an ill-formed sequence
// ends the run and is left to the byte-at-a-time path
if (bytes != 0)
{
put_buffer(string_buffer, bytes);
bytes = 0;
bytes_after_last_accept = 0;
}
const std::size_t escaped = write_u_escapes(reinterpret_cast<const unsigned char*>(s.data()) + i, s.size() - i);
if (escaped != 0)
{
bytes_after_last_accept = 0;
undumped_chars = 0;
i += escaped - 1;
continue;
}
}
switch (decode(state, codepoint, byte)) switch (decode(state, codepoint, byte))
{ {
case UTF8_ACCEPT: // decode found a new code point case UTF8_ACCEPT: // decode found a new code point
@@ -1113,6 +1406,21 @@ class serializer
return; return;
} }
// the usual indentation: four words of the indentation character are
// stored unconditionally and only the part wanted is claimed, which
// spares the call of memset
if (JSON_HEDLEY_LIKELY(indent <= 32 && write_buffer_pos + 32 <= write_buffer.size()))
{
const std::uint64_t word = 0x0101010101010101ull * static_cast<unsigned char>(indent_char);
char* const d = write_buffer.data() + write_buffer_pos;
std::memcpy(d, &word, sizeof(word));
std::memcpy(d + 8, &word, sizeof(word));
std::memcpy(d + 16, &word, sizeof(word));
std::memcpy(d + 24, &word, sizeof(word));
write_buffer_pos += indent;
return;
}
const std::size_t capacity = write_buffer.size(); const std::size_t capacity = write_buffer.size();
// fill whatever room is left in the buffer; this is the whole job // fill whatever room is left in the buffer; this is the whole job
@@ -1146,6 +1454,32 @@ class serializer
write_buffer_pos = indent; write_buffer_pos = indent;
} }
/*!
@brief append a string literal and then @a indent indentation characters
The usual line break and indentation go into the buffer with one check of
its room and four stores of the indentation, see @ref put_indent.
*/
template<std::size_t N>
JSON_HEDLEY_ALWAYS_INLINE void put_literal_indent(const char (&s)[N], const std::size_t indent) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
{
constexpr std::size_t length = N - 1;
if (JSON_HEDLEY_LIKELY(indent <= 32 && write_buffer_pos + length + 32 <= write_buffer.size()))
{
const std::uint64_t word = 0x0101010101010101ull * static_cast<unsigned char>(indent_char);
char* const d = write_buffer.data() + write_buffer_pos;
std::memcpy(d, s, length);
std::memcpy(d + length, &word, sizeof(word));
std::memcpy(d + length + 8, &word, sizeof(word));
std::memcpy(d + length + 16, &word, sizeof(word));
std::memcpy(d + length + 24, &word, sizeof(word));
write_buffer_pos += length + indent;
return;
}
put_literal(s);
put_indent(indent);
}
/*! /*!
@brief append a string literal to the write buffer @brief append a string literal to the write buffer
@@ -1170,6 +1504,28 @@ class serializer
write_buffer_pos += length; write_buffer_pos += length;
} }
/*!
@brief append the characters of @a str in [@a start, @a start + @a length)
A run of up to 16 characters with 16 or more in the string from @a start on
and in the write buffer from its end on is copied as a whole block of 16,
which spares the call of memcpy with a length known only at run time; the
surplus characters in the buffer are overwritten by what follows.
*/
template<typename StringType>
void put_run(const StringType& str, const std::size_t start, const std::size_t length)
{
// a short run is the exception in text without escapes (CJK, accented
// Latin), for which this keeps the long path the fast one
if (JSON_HEDLEY_UNLIKELY(length <= 16 && start + 16 <= str.size() && write_buffer_pos + 16 <= write_buffer.size()))
{
std::memcpy(write_buffer.data() + write_buffer_pos, str.data() + start, 16);
write_buffer_pos += length;
return;
}
put_string(str, start, start + length);
}
/*! /*!
@brief append the characters of @a str in [@a start, @a end) @brief append the characters of @a str in [@a start, @a end)
@@ -1412,6 +1768,75 @@ class serializer
return escapes; return escapes;
} }
/*!
@brief escape the well-formed multi-byte characters at the start of @a data
Writes the "\\uXXXX" escape of each character (two of them for a character
above U+FFFF) straight into the write buffer, which is why the caller must
have written out what is pending in @ref string_buffer. Stops at the first
byte that is below 0x80 or starts an ill-formed or truncated sequence, which
the caller handles.
@return the number of bytes of @a data that have been escaped
*/
JSON_HEDLEY_NEVER_INLINE std::size_t write_u_escapes(const unsigned char* data, std::size_t n)
{
constexpr const char* nibble_to_hex = "0123456789abcdef";
std::size_t i = 0;
while (i < n && data[i] >= 0x80)
{
const std::size_t length = validate_one_utf8(data + i, n - i);
if (length == 0)
{
break;
}
const auto b0 = static_cast<std::uint32_t>(data[i]);
const auto b1 = static_cast<std::uint32_t>(data[i + 1]);
std::uint32_t codepoint = 0;
switch (length)
{
case 2:
codepoint = ((b0 & 0x1Fu) << 6u) | (b1 & 0x3Fu);
break;
case 3:
codepoint = ((b0 & 0x0Fu) << 12u) | ((b1 & 0x3Fu) << 6u) | (static_cast<std::uint32_t>(data[i + 2]) & 0x3Fu);
break;
default:
codepoint = ((b0 & 0x07u) << 18u) | ((b1 & 0x3Fu) << 12u) | ((static_cast<std::uint32_t>(data[i + 2]) & 0x3Fu) << 6u) | (static_cast<std::uint32_t>(data[i + 3]) & 0x3Fu);
break;
}
// at most two escapes of six bytes each
if (JSON_HEDLEY_UNLIKELY(write_buffer.size() - write_buffer_pos < 12))
{
flush();
}
char* d = write_buffer.data() + write_buffer_pos;
if (codepoint > 0xFFFFu)
{
d = put_u_escape(d, static_cast<std::uint16_t>(0xD7C0u + (codepoint >> 10u)), nibble_to_hex);
codepoint = 0xDC00u + (codepoint & 0x3FFu);
}
d = put_u_escape(d, static_cast<std::uint16_t>(codepoint), nibble_to_hex);
write_buffer_pos = static_cast<std::size_t>(d - write_buffer.data());
i += length;
}
return i;
}
static char* put_u_escape(char* d, const std::uint16_t codeunit, const char* nibble_to_hex) noexcept
{
const auto unit = static_cast<std::uint32_t>(codeunit);
d[0] = '\\';
d[1] = 'u';
d[2] = nibble_to_hex[(unit >> 12u) & 0x0Fu];
d[3] = nibble_to_hex[(unit >> 8u) & 0x0Fu];
d[4] = nibble_to_hex[(unit >> 4u) & 0x0Fu];
d[5] = nibble_to_hex[unit & 0x0Fu];
return d + 6;
}
/*! /*!
* @brief write a lowercase "\\uXXXX" escape sequence into @a string_buffer * @brief write a lowercase "\\uXXXX" escape sequence into @a string_buffer
* *
@@ -1769,11 +2194,22 @@ class serializer
/// the output of the serializer (non-owning; the adapter lives at the call site) /// the output of the serializer (non-owning; the adapter lives at the call site)
output_adapter_protocol<char>* o = nullptr; output_adapter_protocol<char>* o = nullptr;
/// a character buffer left uninitialized: the buffers below are written
/// before they are read (only the part written is ever read), and zeroing
/// 1.6 KiB took more than half of what dumping a small value costs (the
/// user-provided constructor keeps the member initialization, which
/// -Weffc++ asks for, from zeroing them)
template<std::size_t N>
struct uninitialized_buffer : std::array<char, N>
{
uninitialized_buffer() noexcept {} // NOLINT(modernize-use-equals-default,hicpp-use-equals-default,cppcoreguidelines-pro-type-member-init,hicpp-member-init): see above
};
/// a (hopefully) large enough character buffer /// a (hopefully) large enough character buffer
std::array<char, 64> number_buffer{{}}; uninitialized_buffer<64> number_buffer;
/// string buffer /// string buffer
std::array<char, 512> string_buffer{{}}; uninitialized_buffer<512> string_buffer;
/// the indentation character /// the indentation character
const char indent_char; const char indent_char;
@@ -1793,7 +2229,7 @@ class serializer
/// buffer collecting output before it is flushed to the output adapter, so /// buffer collecting output before it is flushed to the output adapter, so
/// that the many small structural writes become few bulk writes /// that the many small structural writes become few bulk writes
static constexpr std::size_t write_buffer_size = 1024; static constexpr std::size_t write_buffer_size = 1024;
std::array<char, write_buffer_size> write_buffer{{}}; uninitialized_buffer<write_buffer_size> write_buffer;
/// number of valid bytes currently held in @ref write_buffer /// number of valid bytes currently held in @ref write_buffer
std::size_t write_buffer_pos = 0; std::size_t write_buffer_pos = 0;
}; };
+551 -114
View File
@@ -30178,6 +30178,14 @@ NLOHMANN_JSON_NAMESPACE_END
// #include <nlohmann/detail/value_t.hpp> // #include <nlohmann/detail/value_t.hpp>
// the short-string fast path of the serializer assembles words from little-endian
// loads, see serializer::put_short_quoted()
#if defined(_MSC_VER) || defined(__x86_64__) || defined(__i386__) || (defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
#define JSON_DUMP_SHORT_STRINGS 1
#else
#define JSON_DUMP_SHORT_STRINGS 0
#endif
NLOHMANN_JSON_NAMESPACE_BEGIN NLOHMANN_JSON_NAMESPACE_BEGIN
namespace detail namespace detail
{ {
@@ -30224,11 +30232,14 @@ class serializer
const std::size_t indent_step_ = 0, const std::size_t indent_step_ = 0,
error_handler_t error_handler_ = error_handler_t::strict) error_handler_t error_handler_ = error_handler_t::strict)
: o(&s) : o(&s)
, number_buffer() // NOLINT(readability-redundant-member-init)
, string_buffer() // NOLINT(readability-redundant-member-init)
, indent_char(ichar) , indent_char(ichar)
, pretty_print(pretty_print_) , pretty_print(pretty_print_)
, ensure_ascii(ensure_ascii_) , ensure_ascii(ensure_ascii_)
, indent_step(indent_step_) , indent_step(indent_step_)
, error_handler(error_handler_) , error_handler(error_handler_)
, write_buffer() // NOLINT(readability-redundant-member-init)
{} {}
// deleted because of pointer members // deleted because of pointer members
@@ -30303,34 +30314,27 @@ class serializer
if (pretty_print) if (pretty_print)
{ {
put_literal("{\n");
// variable to hold indentation for recursive calls // variable to hold indentation for recursive calls
const auto new_indent = next_indent(current_indent, indent_step); const auto new_indent = next_indent(current_indent, indent_step);
put_literal_indent("{\n", new_indent);
// first n-1 elements // first n-1 elements
auto i = val.m_data.m_value.object->cbegin(); auto i = val.m_data.m_value.object->cbegin();
for (std::size_t cnt = 0; cnt < val.m_data.m_value.object->size() - 1; ++cnt, ++i) for (std::size_t cnt = 0; cnt < val.m_data.m_value.object->size() - 1; ++cnt, ++i)
{ {
put_indent(new_indent); put_quoted(i->first, "\": ");
put_char('"'); dump_element(i->second, new_indent, depth + 1);
dump_escaped(i->first); put_literal_indent(",\n", new_indent);
put_literal("\": ");
dump_internal(i->second, new_indent, depth + 1);
put_literal(",\n");
} }
// last element // last element
JSON_ASSERT(i != val.m_data.m_value.object->cend()); JSON_ASSERT(i != val.m_data.m_value.object->cend());
JSON_ASSERT(std::next(i) == val.m_data.m_value.object->cend()); JSON_ASSERT(std::next(i) == val.m_data.m_value.object->cend());
put_indent(new_indent); put_quoted(i->first, "\": ");
put_char('"'); dump_element(i->second, new_indent, depth + 1);
dump_escaped(i->first);
put_literal("\": ");
dump_internal(i->second, new_indent, depth + 1);
put_char('\n'); put_literal_indent("\n", current_indent);
put_indent(current_indent);
put_char('}'); put_char('}');
} }
else else
@@ -30341,20 +30345,16 @@ class serializer
auto i = val.m_data.m_value.object->cbegin(); auto i = val.m_data.m_value.object->cbegin();
for (std::size_t cnt = 0; cnt < val.m_data.m_value.object->size() - 1; ++cnt, ++i) for (std::size_t cnt = 0; cnt < val.m_data.m_value.object->size() - 1; ++cnt, ++i)
{ {
put_char('"'); put_quoted(i->first, "\":");
dump_escaped(i->first); dump_element(i->second, current_indent, depth + 1);
put_literal("\":");
dump_internal(i->second, current_indent, depth + 1);
put_char(','); put_char(',');
} }
// last element // last element
JSON_ASSERT(i != val.m_data.m_value.object->cend()); JSON_ASSERT(i != val.m_data.m_value.object->cend());
JSON_ASSERT(std::next(i) == val.m_data.m_value.object->cend()); JSON_ASSERT(std::next(i) == val.m_data.m_value.object->cend());
put_char('"'); put_quoted(i->first, "\":");
dump_escaped(i->first); dump_element(i->second, current_indent, depth + 1);
put_literal("\":");
dump_internal(i->second, current_indent, depth + 1);
put_char('}'); put_char('}');
} }
@@ -30378,27 +30378,24 @@ class serializer
if (pretty_print) if (pretty_print)
{ {
put_literal("[\n");
// variable to hold indentation for recursive calls // variable to hold indentation for recursive calls
const auto new_indent = next_indent(current_indent, indent_step); const auto new_indent = next_indent(current_indent, indent_step);
put_literal_indent("[\n", new_indent);
// first n-1 elements // first n-1 elements
for (auto i = val.m_data.m_value.array->cbegin(); for (auto i = val.m_data.m_value.array->cbegin();
i != val.m_data.m_value.array->cend() - 1; ++i) i != val.m_data.m_value.array->cend() - 1; ++i)
{ {
put_indent(new_indent); dump_element(*i, new_indent, depth + 1);
dump_internal(*i, new_indent, depth + 1); put_literal_indent(",\n", new_indent);
put_literal(",\n");
} }
// last element // last element
JSON_ASSERT(!val.m_data.m_value.array->empty()); JSON_ASSERT(!val.m_data.m_value.array->empty());
put_indent(new_indent); dump_element(val.m_data.m_value.array->back(), new_indent, depth + 1);
dump_internal(val.m_data.m_value.array->back(), new_indent, depth + 1);
put_char('\n'); put_literal_indent("\n", current_indent);
put_indent(current_indent);
put_char(']'); put_char(']');
} }
else else
@@ -30409,13 +30406,13 @@ class serializer
for (auto i = val.m_data.m_value.array->cbegin(); for (auto i = val.m_data.m_value.array->cbegin();
i != val.m_data.m_value.array->cend() - 1; ++i) i != val.m_data.m_value.array->cend() - 1; ++i)
{ {
dump_internal(*i, current_indent, depth + 1); dump_element(*i, current_indent, depth + 1);
put_char(','); put_char(',');
} }
// last element // last element
JSON_ASSERT(!val.m_data.m_value.array->empty()); JSON_ASSERT(!val.m_data.m_value.array->empty());
dump_internal(val.m_data.m_value.array->back(), current_indent, depth + 1); dump_element(val.m_data.m_value.array->back(), current_indent, depth + 1);
put_char(']'); put_char(']');
} }
@@ -30438,6 +30435,27 @@ class serializer
} }
private: private:
/*!
@brief serialize an element of a container
Most elements are scalars, which need none of the state dump_internal() keeps
for a container; writing them from here spares each the entry and exit of
that function.
*/
JSON_HEDLEY_ALWAYS_INLINE void dump_element(const BasicJsonType& val,
const std::size_t current_indent,
const std::size_t depth)
{
if (val.m_data.m_type == value_t::object || val.m_data.m_type == value_t::array)
{
dump_internal(val, current_indent, depth);
}
else
{
dump_scalar(val, current_indent);
}
}
/*! /*!
@brief write out @a val and everything below it without the call stack @brief write out @a val and everything below it without the call stack
@@ -30497,16 +30515,13 @@ class serializer
put_indent(frame.child_indent); put_indent(frame.child_indent);
} }
put_char('"');
dump_escaped(frame.object_it->first);
if (pretty_print) if (pretty_print)
{ {
put_literal("\": "); put_quoted(frame.object_it->first, "\": ");
} }
else else
{ {
put_literal("\":"); put_quoted(frame.object_it->first, "\":");
} }
const BasicJsonType& element = frame.object_it->second; const BasicJsonType& element = frame.object_it->second;
@@ -30681,78 +30696,13 @@ class serializer
{ {
case value_t::string: case value_t::string:
{ {
put_char('"'); dump_string_value(*val.m_data.m_value.string);
dump_escaped(*val.m_data.m_value.string);
put_char('"');
return; return;
} }
case value_t::binary: case value_t::binary:
{ {
if (pretty_print) dump_binary_value(current_indent, *val.m_data.m_value.binary);
{
put_literal("{\n");
// variable to hold indentation for the bytes
const auto new_indent = next_indent(current_indent, indent_step);
put_indent(new_indent);
put_literal("\"bytes\": [");
if (!val.m_data.m_value.binary->empty())
{
for (auto i = val.m_data.m_value.binary->cbegin();
i != val.m_data.m_value.binary->cend() - 1; ++i)
{
dump_byte(*i);
put_literal(", ");
}
dump_byte(val.m_data.m_value.binary->back());
}
put_literal("],\n");
put_indent(new_indent);
put_literal("\"subtype\": ");
if (val.m_data.m_value.binary->has_subtype())
{
dump_integer(val.m_data.m_value.binary->subtype());
}
else
{
put_literal("null");
}
put_char('\n');
put_indent(current_indent);
put_char('}');
}
else
{
put_literal("{\"bytes\":[");
if (!val.m_data.m_value.binary->empty())
{
for (auto i = val.m_data.m_value.binary->cbegin();
i != val.m_data.m_value.binary->cend() - 1; ++i)
{
dump_byte(*i);
put_char(',');
}
dump_byte(val.m_data.m_value.binary->back());
}
put_literal("],\"subtype\":");
if (val.m_data.m_value.binary->has_subtype())
{
dump_integer(val.m_data.m_value.binary->subtype());
put_char('}');
}
else
{
put_literal("null}");
}
}
return; return;
} }
@@ -30771,19 +30721,19 @@ class serializer
case value_t::number_integer: case value_t::number_integer:
{ {
dump_integer(val.m_data.m_value.number_integer); dump_signed_value(val.m_data.m_value.number_integer);
return; return;
} }
case value_t::number_unsigned: case value_t::number_unsigned:
{ {
dump_integer(val.m_data.m_value.number_unsigned); dump_unsigned_value(val.m_data.m_value.number_unsigned);
return; return;
} }
case value_t::number_float: case value_t::number_float:
{ {
dump_float(val.m_data.m_value.number_float); dump_float_value(val.m_data.m_value.number_float);
return; return;
} }
@@ -30806,6 +30756,107 @@ class serializer
} }
} }
/// @brief write the string @a s in quotes; kept out of line, so that the
/// other scalars do not pay for its register needs (and the other way round)
JSON_HEDLEY_NEVER_INLINE void dump_string_value(const string_t& s)
{
#if JSON_DUMP_SHORT_STRINGS
if (s.size() <= short_string_max && put_short_quoted(s, "\""))
{
return;
}
#endif
put_char('"');
dump_escaped(s);
put_char('"');
}
// The numbers, and the rest of what dump_scalar() writes, are left to
// functions of their own as well: a single function holding all of them
// saves and restores every register any of them needs, on every call. Now
// dump_scalar() only dispatches, and each writer pays for what it uses.
JSON_HEDLEY_NEVER_INLINE void dump_signed_value(const number_integer_t x)
{
dump_integer(x);
}
JSON_HEDLEY_NEVER_INLINE void dump_unsigned_value(const number_unsigned_t x)
{
dump_integer(x);
}
JSON_HEDLEY_NEVER_INLINE void dump_float_value(const number_float_t x)
{
dump_float(x);
}
JSON_HEDLEY_NEVER_INLINE void dump_binary_value(const std::size_t current_indent, const typename BasicJsonType::binary_t& binary)
{
if (pretty_print)
{
put_literal("{\n");
// variable to hold indentation for the bytes
const auto new_indent = next_indent(current_indent, indent_step);
put_indent(new_indent);
put_literal("\"bytes\": [");
if (!binary.empty())
{
for (auto i = binary.cbegin();
i != binary.cend() - 1; ++i)
{
dump_byte(*i);
put_literal(", ");
}
dump_byte(binary.back());
}
put_literal("],\n");
put_indent(new_indent);
put_literal("\"subtype\": ");
if (binary.has_subtype())
{
dump_integer(binary.subtype());
}
else
{
put_literal("null");
}
put_char('\n');
put_indent(current_indent);
put_char('}');
}
else
{
put_literal("{\"bytes\":[");
if (!binary.empty())
{
for (auto i = binary.cbegin();
i != binary.cend() - 1; ++i)
{
dump_byte(*i);
put_char(',');
}
dump_byte(binary.back());
}
put_literal("],\"subtype\":");
if (binary.has_subtype())
{
dump_integer(binary.subtype());
put_char('}');
}
else
{
put_literal("null}");
}
}
}
/*! /*!
@brief the indentation level to use for the children of the current value @brief the indentation level to use for the children of the current value
@@ -30822,6 +30873,217 @@ class serializer
return new_indent; return new_indent;
} }
private:
/*!
@brief append the string @a s in quotes, followed by @a tail
The quote, the string and @a tail (the closing quote, possibly with the
colon of an object key) are the bulk of what most documents consist of, and
most keys and many values are short and plain. Those are handled in one go
by put_short_quoted(); everything else takes the general path.
*/
template<std::size_t N>
void put_quoted(const string_t& s, const char (&tail)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
{
#if JSON_DUMP_SHORT_STRINGS
if (put_short_quoted(s, tail))
{
return;
}
#endif
put_char('"');
dump_escaped(s);
put_literal(tail);
}
#if JSON_DUMP_SHORT_STRINGS
/// the longest string put_short_quoted() takes
static constexpr std::size_t short_string_max = JSON_SCAN_VECTOR ? 32 : 16;
/// the high bit of every byte of @a v that is not printable ASCII or is a
/// quote, a backslash, or DEL (zero if the bytes can be copied as they are)
static std::uint64_t swar_not_plain(const std::uint64_t v) noexcept
{
constexpr std::uint64_t ones = 0x0101010101010101ull;
const std::uint64_t q = v ^ 0x2222222222222222ull; // '"'
const std::uint64_t b = v ^ 0x5C5C5C5C5C5C5C5Cull; // '\\'
return (((q - ones) & ~q) | ((b - ones) & ~b) // == '"', == '\\'
| ((v - 0x2020202020202020ull) & ~v) // < 0x20
| v | (v + ones)) // >= 0x7F
& 0x8080808080808080ull;
}
template<typename WordType>
static WordType load_word(const char* p) noexcept
{
WordType w;
std::memcpy(&w, p, sizeof(w));
return w;
}
template<typename WordType>
static void store_word(char* p, const WordType w) noexcept
{
std::memcpy(p, &w, sizeof(w));
}
/*!
@brief write `"`, @a s and @a tail if @a s is short and needs no escaping
A string of up to 16 (32 with vector instructions) printable ASCII
characters (no quote, backslash, or DEL) is read with two overlapping loads
and written with two overlapping stores, instead of scanning it and calling
memcpy with a length that is only known at run time. Returns false, having
changed nothing, if @a s does not qualify or the write buffer lacks the
room.
*/
template<std::size_t N>
bool put_short_quoted(const string_t& s, const char (&tail)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
{
const std::size_t n = s.size();
if (n > short_string_max || write_buffer_pos + n + N > write_buffer.size())
{
return false;
}
char* d = write_buffer.data() + write_buffer_pos + 1;
const char* const p = s.data();
if (n >= 8)
{
#if JSON_SCAN_VECTOR
if (!(n >= 16 ? copy_plain_vectors(d, p, n) : copy_plain_halves(d, p, n)))
{
return false;
}
#else
const auto a = load_word<std::uint64_t>(p);
const auto b = load_word<std::uint64_t>(p + n - 8);
if ((swar_not_plain(a) | swar_not_plain(b)) != 0)
{
return false;
}
store_word(d, a);
store_word(d + n - 8, b);
#endif
}
else if (n >= 4)
{
const auto a = load_word<std::uint32_t>(p);
const auto b = load_word<std::uint32_t>(p + n - 4);
// the bytes above the string are filled with a plain character
const std::uint64_t w = a | (static_cast<std::uint64_t>(b) << (8 * (n - 4)))
| (0x4141414141414141ull << (8 * n));
if (swar_not_plain(w) != 0)
{
return false;
}
store_word(d, a);
store_word(d + n - 4, b);
}
else if (n >= 2)
{
const auto a = load_word<std::uint16_t>(p);
const auto b = load_word<std::uint16_t>(p + n - 2);
const std::uint64_t w = a | (static_cast<std::uint64_t>(b) << (8 * (n - 2)))
| (0x4141414141414141ull << (8 * n));
if (swar_not_plain(w) != 0)
{
return false;
}
store_word(d, a);
store_word(d + n - 2, b);
}
else if (n == 1)
{
const std::uint64_t w = static_cast<std::uint64_t>(static_cast<unsigned char>(p[0])) | (0x4141414141414141ull << 8u);
if (swar_not_plain(w) != 0)
{
return false;
}
*d = p[0];
}
d[-1] = '"';
std::memcpy(d + n, tail, N - 1);
write_buffer_pos += n + N;
return true;
}
#if JSON_SCAN_VECTOR
/// copy the 8 to 15 bytes [@a p, @a p + @a n) to @a d with two overlapping
/// words, tested together as one vector, see copy_plain_vectors()
static bool copy_plain_halves(char* d, const char* p, const std::size_t n) noexcept
{
#if JSON_SCAN_NEON
const uint8x8_t lo = vld1_u8(reinterpret_cast<const std::uint8_t*>(p));
const uint8x8_t hi = vld1_u8(reinterpret_cast<const std::uint8_t*>(p + n - 8));
const uint8x16_t in = vcombine_u8(lo, hi);
const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))),
vorrq_u8(vcltq_s8(vreinterpretq_s8_u8(in), vdupq_n_s8(0x20)), vceqq_u8(in, vdupq_n_u8(0x7F))));
if (vmaxvq_u8(special) != 0)
{
return false;
}
vst1_u8(reinterpret_cast<std::uint8_t*>(d), lo);
vst1_u8(reinterpret_cast<std::uint8_t*>(d + n - 8), hi);
#else
const __m128i lo = _mm_loadl_epi64(static_cast<const __m128i*>(static_cast<const void*>(p))); // NOLINT(bugprone-casting-through-void)
const __m128i hi = _mm_loadl_epi64(static_cast<const __m128i*>(static_cast<const void*>(p + n - 8))); // NOLINT(bugprone-casting-through-void)
const __m128i in = _mm_unpacklo_epi64(lo, hi);
const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))),
_mm_or_si128(_mm_cmplt_epi8(in, _mm_set1_epi8(0x20)), _mm_cmpeq_epi8(in, _mm_set1_epi8(0x7F))));
if (_mm_movemask_epi8(special) != 0)
{
return false;
}
_mm_storel_epi64(static_cast<__m128i*>(static_cast<void*>(d)), lo); // NOLINT(bugprone-casting-through-void)
_mm_storel_epi64(static_cast<__m128i*>(static_cast<void*>(d + n - 8)), hi); // NOLINT(bugprone-casting-through-void)
#endif
return true;
}
/// copy the 16 to 32 bytes [@a p, @a p + @a n) to @a d with two overlapping
/// vectors, unless one of them is a quote, a backslash, a control
/// character, DEL or not ASCII
static bool copy_plain_vectors(char* d, const char* p, const std::size_t n) noexcept
{
#if JSON_SCAN_NEON
const uint8x16_t a = vld1q_u8(reinterpret_cast<const std::uint8_t*>(p));
const uint8x16_t b = vld1q_u8(reinterpret_cast<const std::uint8_t*>(p + n - 16));
const uint8x16_t special = vorrq_u8(vorrq_u8(vorrq_u8(vceqq_u8(a, vdupq_n_u8('"')), vceqq_u8(a, vdupq_n_u8('\\'))),
vorrq_u8(vceqq_u8(b, vdupq_n_u8('"')), vceqq_u8(b, vdupq_n_u8('\\')))),
vorrq_u8(vorrq_u8(vcltq_s8(vreinterpretq_s8_u8(a), vdupq_n_s8(0x20)), vcltq_s8(vreinterpretq_s8_u8(b), vdupq_n_s8(0x20))),
vorrq_u8(vceqq_u8(a, vdupq_n_u8(0x7F)), vceqq_u8(b, vdupq_n_u8(0x7F)))));
if (vmaxvq_u8(special) != 0)
{
return false;
}
vst1q_u8(reinterpret_cast<std::uint8_t*>(d), a);
vst1q_u8(reinterpret_cast<std::uint8_t*>(d + n - 16), b);
#else
const __m128i a = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(p))); // NOLINT(bugprone-casting-through-void)
const __m128i b = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(p + n - 16))); // NOLINT(bugprone-casting-through-void)
const __m128i quote = _mm_set1_epi8('"');
const __m128i backslash = _mm_set1_epi8('\\');
const __m128i space = _mm_set1_epi8(0x20);
const __m128i del = _mm_set1_epi8(0x7F);
const __m128i special = _mm_or_si128(_mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(a, quote), _mm_cmpeq_epi8(a, backslash)),
_mm_or_si128(_mm_cmpeq_epi8(b, quote), _mm_cmpeq_epi8(b, backslash))),
_mm_or_si128(_mm_or_si128(_mm_cmplt_epi8(a, space), _mm_cmplt_epi8(b, space)),
_mm_or_si128(_mm_cmpeq_epi8(a, del), _mm_cmpeq_epi8(b, del))));
if (_mm_movemask_epi8(special) != 0)
{
return false;
}
_mm_storeu_si128(static_cast<__m128i*>(static_cast<void*>(d)), a); // NOLINT(bugprone-casting-through-void)
_mm_storeu_si128(static_cast<__m128i*>(static_cast<void*>(d + n - 16)), b); // NOLINT(bugprone-casting-through-void)
#endif
return true;
}
#endif
#endif
JSON_PRIVATE_UNLESS_TESTED: JSON_PRIVATE_UNLESS_TESTED:
/*! /*!
@brief dump escaped string @brief dump escaped string
@@ -30834,14 +31096,23 @@ class serializer
@param[in] s the string to escape @param[in] s the string to escape
Complexity: Linear in the length of string @a s. Complexity: Linear in the length of string @a s.
@note kept out of line: with the short-string attempts in front of it gone
from here, compilers would otherwise inline it into @ref put_quoted
and then no longer into the loops that call that
*/ */
void dump_escaped(const string_t& s) JSON_HEDLEY_NEVER_INLINE void dump_escaped(const string_t& s)
{ {
// Short strings of printable ASCII - most object keys and many values - // Short strings of printable ASCII - most object keys and many values -
// are checked byte by byte against a table and then copied in one go. // are checked byte by byte against a table and then copied in one go.
// For them, this is cheaper than starting the vector scanner below, // For them, this is cheaper than starting the vector scanner below,
// whose setup costs more than such a string takes to scan. The same // whose setup costs more than such a string takes to scan. The same
// check serves both values of ensure_ascii. // check serves both values of ensure_ascii.
//
// Where put_short_quoted() exists and takes strings of up to 32 bytes,
// the callers have tried it already, and repeating the attempt only
// costs time for strings that need escaping or are not ASCII.
#if !(JSON_DUMP_SHORT_STRINGS && JSON_SCAN_VECTOR)
if (s.size() <= 32) if (s.size() <= 32)
{ {
const auto& copyable = ascii_copyable(); const auto& copyable = ascii_copyable();
@@ -30863,6 +31134,7 @@ class serializer
return; return;
} }
} }
#endif
// dispatch once here rather than test the flag inside the loop: it does // dispatch once here rather than test the flag inside the loop: it does
// not change while a string is written, and folding it lets each of the // not change while a string is written, and folding it lets each of the
@@ -30940,7 +31212,7 @@ class serializer
put_buffer(string_buffer, bytes); put_buffer(string_buffer, bytes);
bytes = 0; bytes = 0;
} }
put_string(s, i, i + run); put_run(s, i, run);
bytes_after_last_accept = 0; bytes_after_last_accept = 0;
undumped_chars = 0; undumped_chars = 0;
i += run; i += run;
@@ -30980,6 +31252,27 @@ class serializer
const auto byte = static_cast<std::uint8_t>(s[i]); const auto byte = static_cast<std::uint8_t>(s[i]);
if (EnsureAscii && state == UTF8_ACCEPT && byte >= 0x80)
{
// a run of well-formed multi-byte characters is escaped
// straight into the write buffer; an ill-formed sequence
// ends the run and is left to the byte-at-a-time path
if (bytes != 0)
{
put_buffer(string_buffer, bytes);
bytes = 0;
bytes_after_last_accept = 0;
}
const std::size_t escaped = write_u_escapes(reinterpret_cast<const unsigned char*>(s.data()) + i, s.size() - i);
if (escaped != 0)
{
bytes_after_last_accept = 0;
undumped_chars = 0;
i += escaped - 1;
continue;
}
}
switch (decode(state, codepoint, byte)) switch (decode(state, codepoint, byte))
{ {
case UTF8_ACCEPT: // decode found a new code point case UTF8_ACCEPT: // decode found a new code point
@@ -31257,6 +31550,21 @@ class serializer
return; return;
} }
// the usual indentation: four words of the indentation character are
// stored unconditionally and only the part wanted is claimed, which
// spares the call of memset
if (JSON_HEDLEY_LIKELY(indent <= 32 && write_buffer_pos + 32 <= write_buffer.size()))
{
const std::uint64_t word = 0x0101010101010101ull * static_cast<unsigned char>(indent_char);
char* const d = write_buffer.data() + write_buffer_pos;
std::memcpy(d, &word, sizeof(word));
std::memcpy(d + 8, &word, sizeof(word));
std::memcpy(d + 16, &word, sizeof(word));
std::memcpy(d + 24, &word, sizeof(word));
write_buffer_pos += indent;
return;
}
const std::size_t capacity = write_buffer.size(); const std::size_t capacity = write_buffer.size();
// fill whatever room is left in the buffer; this is the whole job // fill whatever room is left in the buffer; this is the whole job
@@ -31290,6 +31598,32 @@ class serializer
write_buffer_pos = indent; write_buffer_pos = indent;
} }
/*!
@brief append a string literal and then @a indent indentation characters
The usual line break and indentation go into the buffer with one check of
its room and four stores of the indentation, see @ref put_indent.
*/
template<std::size_t N>
JSON_HEDLEY_ALWAYS_INLINE void put_literal_indent(const char (&s)[N], const std::size_t indent) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
{
constexpr std::size_t length = N - 1;
if (JSON_HEDLEY_LIKELY(indent <= 32 && write_buffer_pos + length + 32 <= write_buffer.size()))
{
const std::uint64_t word = 0x0101010101010101ull * static_cast<unsigned char>(indent_char);
char* const d = write_buffer.data() + write_buffer_pos;
std::memcpy(d, s, length);
std::memcpy(d + length, &word, sizeof(word));
std::memcpy(d + length + 8, &word, sizeof(word));
std::memcpy(d + length + 16, &word, sizeof(word));
std::memcpy(d + length + 24, &word, sizeof(word));
write_buffer_pos += length + indent;
return;
}
put_literal(s);
put_indent(indent);
}
/*! /*!
@brief append a string literal to the write buffer @brief append a string literal to the write buffer
@@ -31314,6 +31648,28 @@ class serializer
write_buffer_pos += length; write_buffer_pos += length;
} }
/*!
@brief append the characters of @a str in [@a start, @a start + @a length)
A run of up to 16 characters with 16 or more in the string from @a start on
and in the write buffer from its end on is copied as a whole block of 16,
which spares the call of memcpy with a length known only at run time; the
surplus characters in the buffer are overwritten by what follows.
*/
template<typename StringType>
void put_run(const StringType& str, const std::size_t start, const std::size_t length)
{
// a short run is the exception in text without escapes (CJK, accented
// Latin), for which this keeps the long path the fast one
if (JSON_HEDLEY_UNLIKELY(length <= 16 && start + 16 <= str.size() && write_buffer_pos + 16 <= write_buffer.size()))
{
std::memcpy(write_buffer.data() + write_buffer_pos, str.data() + start, 16);
write_buffer_pos += length;
return;
}
put_string(str, start, start + length);
}
/*! /*!
@brief append the characters of @a str in [@a start, @a end) @brief append the characters of @a str in [@a start, @a end)
@@ -31556,6 +31912,75 @@ class serializer
return escapes; return escapes;
} }
/*!
@brief escape the well-formed multi-byte characters at the start of @a data
Writes the "\\uXXXX" escape of each character (two of them for a character
above U+FFFF) straight into the write buffer, which is why the caller must
have written out what is pending in @ref string_buffer. Stops at the first
byte that is below 0x80 or starts an ill-formed or truncated sequence, which
the caller handles.
@return the number of bytes of @a data that have been escaped
*/
JSON_HEDLEY_NEVER_INLINE std::size_t write_u_escapes(const unsigned char* data, std::size_t n)
{
constexpr const char* nibble_to_hex = "0123456789abcdef";
std::size_t i = 0;
while (i < n && data[i] >= 0x80)
{
const std::size_t length = validate_one_utf8(data + i, n - i);
if (length == 0)
{
break;
}
const auto b0 = static_cast<std::uint32_t>(data[i]);
const auto b1 = static_cast<std::uint32_t>(data[i + 1]);
std::uint32_t codepoint = 0;
switch (length)
{
case 2:
codepoint = ((b0 & 0x1Fu) << 6u) | (b1 & 0x3Fu);
break;
case 3:
codepoint = ((b0 & 0x0Fu) << 12u) | ((b1 & 0x3Fu) << 6u) | (static_cast<std::uint32_t>(data[i + 2]) & 0x3Fu);
break;
default:
codepoint = ((b0 & 0x07u) << 18u) | ((b1 & 0x3Fu) << 12u) | ((static_cast<std::uint32_t>(data[i + 2]) & 0x3Fu) << 6u) | (static_cast<std::uint32_t>(data[i + 3]) & 0x3Fu);
break;
}
// at most two escapes of six bytes each
if (JSON_HEDLEY_UNLIKELY(write_buffer.size() - write_buffer_pos < 12))
{
flush();
}
char* d = write_buffer.data() + write_buffer_pos;
if (codepoint > 0xFFFFu)
{
d = put_u_escape(d, static_cast<std::uint16_t>(0xD7C0u + (codepoint >> 10u)), nibble_to_hex);
codepoint = 0xDC00u + (codepoint & 0x3FFu);
}
d = put_u_escape(d, static_cast<std::uint16_t>(codepoint), nibble_to_hex);
write_buffer_pos = static_cast<std::size_t>(d - write_buffer.data());
i += length;
}
return i;
}
static char* put_u_escape(char* d, const std::uint16_t codeunit, const char* nibble_to_hex) noexcept
{
const auto unit = static_cast<std::uint32_t>(codeunit);
d[0] = '\\';
d[1] = 'u';
d[2] = nibble_to_hex[(unit >> 12u) & 0x0Fu];
d[3] = nibble_to_hex[(unit >> 8u) & 0x0Fu];
d[4] = nibble_to_hex[(unit >> 4u) & 0x0Fu];
d[5] = nibble_to_hex[unit & 0x0Fu];
return d + 6;
}
/*! /*!
* @brief write a lowercase "\\uXXXX" escape sequence into @a string_buffer * @brief write a lowercase "\\uXXXX" escape sequence into @a string_buffer
* *
@@ -31913,11 +32338,22 @@ class serializer
/// the output of the serializer (non-owning; the adapter lives at the call site) /// the output of the serializer (non-owning; the adapter lives at the call site)
output_adapter_protocol<char>* o = nullptr; output_adapter_protocol<char>* o = nullptr;
/// a character buffer left uninitialized: the buffers below are written
/// before they are read (only the part written is ever read), and zeroing
/// 1.6 KiB took more than half of what dumping a small value costs (the
/// user-provided constructor keeps the member initialization, which
/// -Weffc++ asks for, from zeroing them)
template<std::size_t N>
struct uninitialized_buffer : std::array<char, N>
{
uninitialized_buffer() noexcept {} // NOLINT(modernize-use-equals-default,hicpp-use-equals-default,cppcoreguidelines-pro-type-member-init,hicpp-member-init): see above
};
/// a (hopefully) large enough character buffer /// a (hopefully) large enough character buffer
std::array<char, 64> number_buffer{{}}; uninitialized_buffer<64> number_buffer;
/// string buffer /// string buffer
std::array<char, 512> string_buffer{{}}; uninitialized_buffer<512> string_buffer;
/// the indentation character /// the indentation character
const char indent_char; const char indent_char;
@@ -31937,7 +32373,7 @@ class serializer
/// buffer collecting output before it is flushed to the output adapter, so /// buffer collecting output before it is flushed to the output adapter, so
/// that the many small structural writes become few bulk writes /// that the many small structural writes become few bulk writes
static constexpr std::size_t write_buffer_size = 1024; static constexpr std::size_t write_buffer_size = 1024;
std::array<char, write_buffer_size> write_buffer{{}}; uninitialized_buffer<write_buffer_size> write_buffer;
/// number of valid bytes currently held in @ref write_buffer /// number of valid bytes currently held in @ref write_buffer
std::size_t write_buffer_pos = 0; std::size_t write_buffer_pos = 0;
}; };
@@ -40701,6 +41137,7 @@ struct formatter<nlohmann::NLOHMANN_BASIC_JSON_TPL, char> // NOLINT(cert-dcl58-c
#undef JSON_SCAN_SSSE3_TARGET #undef JSON_SCAN_SSSE3_TARGET
#undef JSON_SCAN_VECTOR #undef JSON_SCAN_VECTOR
#undef JSON_SCAN_VECTOR_UTF8 #undef JSON_SCAN_VECTOR_UTF8
#undef JSON_DUMP_SHORT_STRINGS
#ifndef JSON_TEST_KEEP_MACROS #ifndef JSON_TEST_KEEP_MACROS
#undef JSON_CATCH #undef JSON_CATCH
+91
View File
@@ -1580,3 +1580,94 @@ TEST_CASE("invalid UTF-8 at every position is handled like the reference")
} }
} }
} }
TEST_CASE("short strings are written like the reference, as values and as keys")
{
// strings of up to 16 characters (32 with vector instructions) that need no
// escaping are written as a whole (together with the quotes and, for a key,
// the colon); everything else, including a string that is merely short,
// takes the general path.
// Every length around the edges of the size classes of the fast path is
// combined with each kind of byte that must send a string to the general
// path, at each position, and compared with the reference for values and,
// via the value, for keys.
const std::vector<std::string> specials =
{
"\"", "\\", "\n", "\x1f", "\x7f", "\xc3\xa9", "\xe2\x82\xac", "\xf0\x9f\x98\x80", "\xff", "\x80", "\xc3"
};
for (std::size_t length = 0; length <= 36; ++length)
{
const std::string plain(length, 'k');
check_dump_against_reference(plain, false);
for (const auto& special : specials)
{
for (std::size_t pos = 0; pos <= length; ++pos)
{
std::string s = plain;
s.insert(pos, special);
check_dump_against_reference(s, false);
for (const bool ensure_ascii :
{
false, true
})
{
// a key and a value are written alike, in compact and
// in pretty-printed form
const json value = s;
const json object = json::object({{s, 1}});
for (const auto error_handler :
{
json::error_handler_t::replace, json::error_handler_t::keep
})
{
const std::string quoted = value.dump(-1, ' ', ensure_ascii, error_handler);
CHECK(object.dump(-1, ' ', ensure_ascii, error_handler) == "{" + quoted + ":1}");
CHECK(object.dump(2, ' ', ensure_ascii, error_handler) == "{\n " + quoted + ": 1\n}");
}
}
}
}
}
}
TEST_CASE("runs of multi-byte characters are escaped like the reference under ensure_ascii")
{
// a run of well-formed multi-byte characters is escaped straight into the
// write buffer; it has to flush that mid-run, stop at an ASCII character or
// an ill-formed sequence and leave those to the general path, and pick up
// behind what the general path has pending
const std::vector<std::string> characters =
{
"\xc2\x80", "\xdf\xbf", "\xe0\xa0\x80", "\xe2\x82\xac", "\xed\x9f\xbf", "\xef\xbf\xbf",
"\xf0\x90\x80\x80", "\xf0\x9f\x98\x80", "\xf4\x8f\xbf\xbf"
};
const std::vector<std::string> interruptions =
{
"", "a", "\n", "\"", "\x7f", "\xff", "\x80", "\xc3", "\xe2\x82", "\xf0\x9f\x98", "\xed\xa0\x80"
};
for (std::size_t shift = 0; shift <= 13; ++shift)
{
for (const auto& interruption : interruptions)
{
for (const std::size_t count :
{
std::size_t{1}, std::size_t{2}, std::size_t{85}, std::size_t{200}
})
{
std::string s(shift, 'x');
for (std::size_t i = 0; i < count; ++i)
{
s += characters[(i + shift) % characters.size()];
}
s += interruption;
s += characters[shift % characters.size()];
s += characters[(shift + 3) % characters.size()];
check_dump_against_reference(s, shift < 2);
}
}
}
}