From cc946adf6893537f571f585ca17d1de196c5e1f6 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 21 Oct 2026 22:38:46 +0200 Subject: [PATCH] Find the end of the plain run of a decoded string 16 bytes at a time with SSE2 The strings of a document that had escape sequences are written in runs without a quote, backslash, or control character; the compact dump found the end of a run 8 bytes at a time with a SWAR test. With SSE2 (x86-64) it now compares 16 bytes per step. NEON keeps the SWAR code: there the mask of a step costs more than it saves on the short runs that are typical here. Instructions of dump(), Linux (GCC 13 / Clang 18): twitter.json 1.99M -> 1.88M (-5.2 %) / 1.97M -> 1.92M (-3.0 %) CJK text with \u escapes 5.98M -> 4.52M (-24.5 %) / 5.30M -> 4.25M (-19.9 %) ASCII text with a few escapes 4.28M -> 4.09M (-4.4 %) / 4.24M -> 4.11M (-3.2 %) jeopardy (4,000 questions) 5.08M -> 4.78M (-5.8 %) / 4.97M -> 4.80M (-3.4 %) Signed-off-by: Niels Lohmann --- include/nlohmann/detail/view/serializer.hpp | 18 +++++++++++++++++- single_include/nlohmann/json_view.hpp | 18 +++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/include/nlohmann/detail/view/serializer.hpp b/include/nlohmann/detail/view/serializer.hpp index 061780cf5..3dc007bff 100644 --- a/include/nlohmann/detail/view/serializer.hpp +++ b/include/nlohmann/detail/view/serializer.hpp @@ -137,9 +137,25 @@ class output_buffer /// image_check::bounds can have others, which are then written unchanged). inline std::size_t plain_output_run(const unsigned char* s, std::size_t n) noexcept { + std::size_t i = 0; +#if NLOHMANN_VIEW_SSE2 + // (16 bytes per step; with NEON, the mask of a step costs more than it saves on the + // short runs that are typical here) + for (; i + 16 <= n; i += 16) + { + const __m128i in = _mm_loadu_si128(static_cast(static_cast(s + i))); // NOLINT(bugprone-casting-through-void) + // (a byte is below 0x20 if it is its own minimum with 0x1F) + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), + _mm_cmpeq_epi8(_mm_min_epu8(in, _mm_set1_epi8(0x1F)), in)); + const auto mask = static_cast(static_cast(_mm_movemask_epi8(special))); + if (mask != 0) + { + return i + static_cast(count_trailing_zeros(mask)); + } + } +#endif constexpr std::uint64_t ones = 0x0101010101010101ull; constexpr std::uint64_t high = 0x8080808080808080ull; - std::size_t i = 0; for (; i + 8 <= n; i += 8) { const std::uint64_t v = read_eight_bytes(s + i); diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index e54d2981e..a80dabd60 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -6204,9 +6204,25 @@ class output_buffer /// image_check::bounds can have others, which are then written unchanged). inline std::size_t plain_output_run(const unsigned char* s, std::size_t n) noexcept { + std::size_t i = 0; +#if NLOHMANN_VIEW_SSE2 + // (16 bytes per step; with NEON, the mask of a step costs more than it saves on the + // short runs that are typical here) + for (; i + 16 <= n; i += 16) + { + const __m128i in = _mm_loadu_si128(static_cast(static_cast(s + i))); // NOLINT(bugprone-casting-through-void) + // (a byte is below 0x20 if it is its own minimum with 0x1F) + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), + _mm_cmpeq_epi8(_mm_min_epu8(in, _mm_set1_epi8(0x1F)), in)); + const auto mask = static_cast(static_cast(_mm_movemask_epi8(special))); + if (mask != 0) + { + return i + static_cast(count_trailing_zeros(mask)); + } + } +#endif constexpr std::uint64_t ones = 0x0101010101010101ull; constexpr std::uint64_t high = 0x8080808080808080ull; - std::size_t i = 0; for (; i + 8 <= n; i += 8) { const std::uint64_t v = read_eight_bytes(s + i);