diff --git a/include/nlohmann/detail/output/serializer.hpp b/include/nlohmann/detail/output/serializer.hpp index 1dd5e41e3..7bb4edfc5 100644 --- a/include/nlohmann/detail/output/serializer.hpp +++ b/include/nlohmann/detail/output/serializer.hpp @@ -693,6 +693,33 @@ class serializer */ void dump_escaped(const string_t& s) { + // 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. + // For them, this is cheaper than starting the vector scanner below, + // whose setup costs more than such a string takes to scan. The same + // check serves both values of ensure_ascii. + if (s.size() <= 32) + { + const auto& copyable = ascii_copyable(); + const auto* const data = reinterpret_cast(s.data()); + const std::size_t n = s.size(); + std::size_t k = 0; + while (k + 4 <= n && copyable[data[k]] != 0 && copyable[data[k + 1]] != 0 + && copyable[data[k + 2]] != 0 && copyable[data[k + 3]] != 0) + { + k += 4; + } + while (k < n && copyable[data[k]] != 0) + { + ++k; + } + if (k == n) + { + put_string(s, 0, n); + return; + } + } + // 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 // two scanners be inlined into a loop of its own @@ -1073,8 +1100,12 @@ class serializer the indentation character once and flushing that same content repeatedly: flushing does not disturb what the buffer holds, so re-filling it between flushes would be redundant work. + + @note inlined on purpose, as is @ref put_literal: GCC otherwise stops + inlining both into the pretty-printing loops once @ref dump_escaped + has grown, which made indented output up to 13% slower */ - void put_indent(std::size_t indent) + JSON_HEDLEY_ALWAYS_INLINE void put_indent(std::size_t indent) { // closing braces at the outermost level ask for no indentation at all if (indent == 0) @@ -1124,7 +1155,7 @@ class serializer write-through path for oversized runs. */ template - void put_literal(const char (&s)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + JSON_HEDLEY_ALWAYS_INLINE void put_literal(const char (&s)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) { static_assert(N >= 2, "put_literal expects a non-empty string literal"); // the array bound counts the terminating NUL, which is not written @@ -1233,6 +1264,39 @@ class serializer } } + /*! + @brief whether each byte can be written verbatim under either value of ensure_ascii + + Entry c is 1 for printable ASCII other than '"' and '\\' (see + is_ascii_copyable()) and 0 otherwise. A table lookup per byte is the + cheapest test for the short strings handled first in dump_escaped(). + */ + static const std::array& ascii_copyable() noexcept + { + static const std::array copyable = + { + { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x00 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x10 + 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x20 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x30 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x40 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 0x50 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x60 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // 0x70 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x80 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x90 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xA0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xB0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xC0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xD0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xE0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // 0xF0 + } + }; + return copyable; + } + /*! @brief the JSON form of each character below 0x60 diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 0ed2c6358..164ad22dd 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -30558,6 +30558,33 @@ class serializer */ void dump_escaped(const string_t& s) { + // 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. + // For them, this is cheaper than starting the vector scanner below, + // whose setup costs more than such a string takes to scan. The same + // check serves both values of ensure_ascii. + if (s.size() <= 32) + { + const auto& copyable = ascii_copyable(); + const auto* const data = reinterpret_cast(s.data()); + const std::size_t n = s.size(); + std::size_t k = 0; + while (k + 4 <= n && copyable[data[k]] != 0 && copyable[data[k + 1]] != 0 + && copyable[data[k + 2]] != 0 && copyable[data[k + 3]] != 0) + { + k += 4; + } + while (k < n && copyable[data[k]] != 0) + { + ++k; + } + if (k == n) + { + put_string(s, 0, n); + return; + } + } + // 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 // two scanners be inlined into a loop of its own @@ -30938,8 +30965,12 @@ class serializer the indentation character once and flushing that same content repeatedly: flushing does not disturb what the buffer holds, so re-filling it between flushes would be redundant work. + + @note inlined on purpose, as is @ref put_literal: GCC otherwise stops + inlining both into the pretty-printing loops once @ref dump_escaped + has grown, which made indented output up to 13% slower */ - void put_indent(std::size_t indent) + JSON_HEDLEY_ALWAYS_INLINE void put_indent(std::size_t indent) { // closing braces at the outermost level ask for no indentation at all if (indent == 0) @@ -30989,7 +31020,7 @@ class serializer write-through path for oversized runs. */ template - void put_literal(const char (&s)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + JSON_HEDLEY_ALWAYS_INLINE void put_literal(const char (&s)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) { static_assert(N >= 2, "put_literal expects a non-empty string literal"); // the array bound counts the terminating NUL, which is not written @@ -31098,6 +31129,39 @@ class serializer } } + /*! + @brief whether each byte can be written verbatim under either value of ensure_ascii + + Entry c is 1 for printable ASCII other than '"' and '\\' (see + is_ascii_copyable()) and 0 otherwise. A table lookup per byte is the + cheapest test for the short strings handled first in dump_escaped(). + */ + static const std::array& ascii_copyable() noexcept + { + static const std::array copyable = + { + { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x00 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x10 + 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x20 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x30 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x40 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 0x50 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x60 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // 0x70 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x80 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x90 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xA0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xB0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xC0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xD0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xE0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // 0xF0 + } + }; + return copyable; + } + /*! @brief the JSON form of each character below 0x60 diff --git a/tests/src/unit-serialization.cpp b/tests/src/unit-serialization.cpp index b6608a49a..5722c75a0 100644 --- a/tests/src/unit-serialization.cpp +++ b/tests/src/unit-serialization.cpp @@ -1417,6 +1417,27 @@ void check_dump_against_reference(const std::string& s, const bool with_strict = } } // namespace +TEST_CASE("every ASCII character at every position of a short string is written like the reference") +{ + // dump_escaped() copies strings of up to 32 bytes of printable ASCII after + // a table lookup per byte, and hands everything else to the scanners; put + // each ASCII character and a 2-byte character at every position of + // strings of up to 40 bytes to cover both sides of that bound + for (std::size_t length = 1; length <= 40; ++length) + { + for (std::size_t pos = 0; pos < length; ++pos) + { + for (int c = 0; c < 0x80; ++c) + { + std::string s(length, 'a'); + s[pos] = static_cast(c); + check_dump_against_reference(s); + } + check_dump_against_reference(std::string(pos, 'a') + "\xC3\xA4" + std::string(length - pos - 1, 'a')); + } + } +} + TEST_CASE("invalid UTF-8 at every position is handled like the reference") { // ill-formed byte sequences; those that end in a truncated sequence are