From 0c3e83325a0ff8ed4ad2ff33329aa765d32062ea Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 16 Oct 2026 22:25:44 +0200 Subject: [PATCH] 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 --- include/nlohmann/detail/output/serializer.hpp | 39 +++++++++++++++++++ single_include/nlohmann/json.hpp | 39 +++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/include/nlohmann/detail/output/serializer.hpp b/include/nlohmann/detail/output/serializer.hpp index 6c30cf4cc..fc8547a06 100644 --- a/include/nlohmann/detail/output/serializer.hpp +++ b/include/nlohmann/detail/output/serializer.hpp @@ -815,6 +815,12 @@ class serializer else #endif { +#if JSON_SCAN_VECTOR + if (!copy_plain_halves(d, p, n)) + { + return false; + } +#else const auto a = load_word(p); const auto b = load_word(p + n - 8); if ((swar_not_plain(a) | swar_not_plain(b)) != 0) @@ -823,6 +829,7 @@ class serializer } store_word(d, a); store_word(d + n - 8, b); +#endif } } else if (n >= 4) @@ -869,6 +876,38 @@ class serializer } #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(p)); + const uint8x8_t hi = vld1_u8(reinterpret_cast(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(d), lo); + vst1_u8(reinterpret_cast(d + n - 8), hi); +#else + const __m128i lo = _mm_loadl_epi64(static_cast(static_cast(p))); // NOLINT(bugprone-casting-through-void) + const __m128i hi = _mm_loadl_epi64(static_cast(static_cast(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(d)), lo); // NOLINT(bugprone-casting-through-void) + _mm_storel_epi64(static_cast<__m128i*>(static_cast(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 diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 364ca1aad..ba36d8d8a 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -30959,6 +30959,12 @@ class serializer else #endif { +#if JSON_SCAN_VECTOR + if (!copy_plain_halves(d, p, n)) + { + return false; + } +#else const auto a = load_word(p); const auto b = load_word(p + n - 8); if ((swar_not_plain(a) | swar_not_plain(b)) != 0) @@ -30967,6 +30973,7 @@ class serializer } store_word(d, a); store_word(d + n - 8, b); +#endif } } else if (n >= 4) @@ -31013,6 +31020,38 @@ class serializer } #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(p)); + const uint8x8_t hi = vld1_u8(reinterpret_cast(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(d), lo); + vst1_u8(reinterpret_cast(d + n - 8), hi); +#else + const __m128i lo = _mm_loadl_epi64(static_cast(static_cast(p))); // NOLINT(bugprone-casting-through-void) + const __m128i hi = _mm_loadl_epi64(static_cast(static_cast(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(d)), lo); // NOLINT(bugprone-casting-through-void) + _mm_storel_epi64(static_cast<__m128i*>(static_cast(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