From 8262acca0ea4b9f1d3ec69514c071b12620fc0df Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 23 Oct 2026 10:16:27 +0200 Subject: [PATCH] serializer: constexpr ascii table, hex4_lower, dedupe helpers (#5891) 1.1: build ascii_copyable() at compile time from is_ascii_copyable and test find_ascii_copyable_run against it for every byte. 1.2/2.5.4: use detail::hex4_lower in both \uXXXX writers. 2.1.6: factor the repeated flush/emit blocks in dump_escaped_impl into always-inline helpers. 2.1.7: comment why the escape copy does not use put_buffer. Signed-off-by: Niels Lohmann --- include/nlohmann/detail/output/serializer.hpp | 115 +++++++----------- tests/src/unit-class_lexer.cpp | 29 +++++ 2 files changed, 73 insertions(+), 71 deletions(-) diff --git a/include/nlohmann/detail/output/serializer.hpp b/include/nlohmann/detail/output/serializer.hpp index 5eb9906a1..598fda98a 100644 --- a/include/nlohmann/detail/output/serializer.hpp +++ b/include/nlohmann/detail/output/serializer.hpp @@ -966,6 +966,28 @@ class serializer } } + /// write out the bytes pending in @ref string_buffer and reset @a bytes + JSON_HEDLEY_ALWAYS_INLINE void emit_pending(std::size_t& bytes) + { + if (bytes != 0) + { + put_buffer(string_buffer, bytes); + bytes = 0; + } + } + + /// write buffer and reset index if fewer than 13 bytes are left, as this + /// is the maximal number of bytes to be written ("\uxxxx\uxxxx\0") for one + /// code point + JSON_HEDLEY_ALWAYS_INLINE void reserve_string_buffer(std::size_t& bytes) + { + if (string_buffer.size() - bytes < 13) + { + put_buffer(string_buffer, bytes); + bytes = 0; + } + } + /*! @brief worker for @ref dump_escaped @@ -1024,11 +1046,7 @@ class serializer { // emit any bytes still pending in string_buffer first to // preserve output order, then write the run directly - if (bytes != 0) - { - put_buffer(string_buffer, bytes); - bytes = 0; - } + emit_pending(bytes); put_run(s, i, run); bytes_after_last_accept = 0; undumped_chars = 0; @@ -1049,11 +1067,10 @@ class serializer { // emit any bytes still pending in string_buffer first to // preserve output order - if (bytes != 0) - { - put_buffer(string_buffer, bytes); - bytes = 0; - } + emit_pending(bytes); + // deliberately not put_buffer(): a fixed 8-byte memcpy + // has no asserts and no write-through branch, which + // matters on the hot path const auto& escape = ascii_escapes()[data[i]]; if (JSON_HEDLEY_UNLIKELY(write_buffer.size() - write_buffer_pos < escape.size())) { @@ -1128,14 +1145,7 @@ class serializer string_buffer[bytes++] = s[i]; } - // write buffer and reset index; there must be 13 bytes - // left, as this is the maximal number of bytes to be - // written ("\uxxxx\uxxxx\0") for one code point - if (string_buffer.size() - bytes < 13) - { - put_buffer(string_buffer, bytes); - bytes = 0; - } + reserve_string_buffer(bytes); // remember the byte position of this accept bytes_after_last_accept = bytes; @@ -1187,14 +1197,7 @@ class serializer string_buffer[bytes++] = '\xBD'; } - // write buffer and reset index; there must be 13 bytes - // left, as this is the maximal number of bytes to be - // written ("\uxxxx\uxxxx\0") for one code point - if (string_buffer.size() - bytes < 13) - { - put_buffer(string_buffer, bytes); - bytes = 0; - } + reserve_string_buffer(bytes); bytes_after_last_accept = bytes; } @@ -1227,14 +1230,7 @@ class serializer string_buffer[bytes++] = s[i]; } - // write buffer and reset index; there must be 13 bytes - // left, as this is the maximal number of bytes to be - // written ("\uxxxx\uxxxx\0") for one code point - if (string_buffer.size() - bytes < 13) - { - put_buffer(string_buffer, bytes); - bytes = 0; - } + reserve_string_buffer(bytes); bytes_after_last_accept = bytes; undumped_chars = 0; @@ -1600,30 +1596,17 @@ class serializer */ 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 - } - }; + // constant-initialized from the predicate: no runtime loop, no guard + static constexpr std::array copyable = make_ascii_copyable(make_index_sequence<256> {}); return copyable; } + template + static constexpr std::array make_ascii_copyable(index_sequence /*unused*/) noexcept + { + return {{ static_cast(is_ascii_copyable(static_cast(Is)) ? 1 : 0)... }}; + } + /*! @brief the JSON form of each character below 0x60 @@ -1752,7 +1735,6 @@ class serializer */ 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) { @@ -1789,26 +1771,21 @@ class serializer char* d = write_buffer.data() + write_buffer_pos; if (codepoint > 0xFFFFu) { - d = put_u_escape(d, static_cast(0xD7C0u + (codepoint >> 10u)), nibble_to_hex); + d = put_u_escape(d, static_cast(0xD7C0u + (codepoint >> 10u))); codepoint = 0xDC00u + (codepoint & 0x3FFu); } - d = put_u_escape(d, static_cast(codepoint), nibble_to_hex); + d = put_u_escape(d, static_cast(codepoint)); write_buffer_pos = static_cast(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 + static char* put_u_escape(char* d, const std::uint16_t codeunit) noexcept { - const auto unit = static_cast(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; + return hex4_lower(codeunit, d + 2); } /*! @@ -1817,7 +1794,7 @@ class serializer * Branch-free replacement for `snprintf(buf, 7, "\\u%04x", codeunit)` in the * string escaping hot path. It writes exactly six characters ('\\', 'u' and * four hex digits) at position @a pos of @a string_buffer via a nibble - * lookup table, avoiding the format-string parsing and locale machinery of + * lookup table (@ref hex4_lower), avoiding the format-string parsing and locale machinery of * `snprintf`. Advances @a pos by the number of bytes written (6). * * @param[in] pos position in @a string_buffer to write at; there must @@ -1827,13 +1804,9 @@ class serializer void write_u_escape(std::size_t& pos, std::uint16_t codeunit) noexcept { JSON_ASSERT(string_buffer.size() - pos >= 6); - constexpr const char* nibble_to_hex = "0123456789abcdef"; string_buffer[pos + 0] = '\\'; string_buffer[pos + 1] = 'u'; - string_buffer[pos + 2] = nibble_to_hex[(codeunit >> 12u) & 0x0Fu]; - string_buffer[pos + 3] = nibble_to_hex[(codeunit >> 8u) & 0x0Fu]; - string_buffer[pos + 4] = nibble_to_hex[(codeunit >> 4u) & 0x0Fu]; - string_buffer[pos + 5] = nibble_to_hex[codeunit & 0x0Fu]; + hex4_lower(codeunit, &string_buffer[pos + 2]); pos += 6; } diff --git a/tests/src/unit-class_lexer.cpp b/tests/src/unit-class_lexer.cpp index fac88687f..c35d8e1a1 100644 --- a/tests/src/unit-class_lexer.cpp +++ b/tests/src/unit-class_lexer.cpp @@ -2042,3 +2042,32 @@ TEST_CASE("string scanning kernels") CHECK(nlohmann::detail::read_eight_bytes(bytes.data() + 8) == 0xFF0F0E0D0C0B0A09u); CHECK(nlohmann::detail::read_eight_bytes(reinterpret_cast(bytes.data()) + 3) == 0x0B0A090807060504u); } + +TEST_CASE("find_ascii_copyable_run agrees with is_ascii_copyable for every byte") +{ + // the SWAR mask in find_ascii_copyable_run() encodes the predicate a + // second time; check every byte value at every position of a 24-byte buffer + // (three words) and for several lengths, with the byte at the end or inside + for (unsigned int value = 0; value < 256; ++value) + { + for (std::size_t position = 0; position < 24; ++position) + { + std::array buffer{}; + buffer.fill(static_cast('a')); + buffer[position] = static_cast(value); + const bool copyable = nlohmann::detail::is_ascii_copyable(static_cast(value)); + for (const std::size_t n : {position, position + 1, std::size_t{24}}) + { + if (n > 24) + { + continue; + } + const std::size_t expected = (copyable || n <= position) ? n : position; + CAPTURE(value) + CAPTURE(position) + CAPTURE(n) + CHECK(nlohmann::detail::find_ascii_copyable_run(buffer.data(), n) == expected); + } + } + } +}