From 5a5d33b513d372b4eefe2b6fa96fab2c4dfc62e6 Mon Sep 17 00:00:00 2001 From: mrabine Date: Sun, 25 Oct 2026 23:38:01 +0100 Subject: [PATCH] Optimize json and msgpack hot paths --- core/include/join/utils.hpp | 11 ++++ core/include/join/variant.hpp | 16 +++-- core/tests/utils_test.cpp | 10 +++ data/include/join/document.hpp | 15 +---- data/include/join/dtoa.hpp | 17 +++-- data/include/join/json.hpp | 111 ++++++++++++++++++++++---------- data/include/join/pack.hpp | 20 ++++-- data/include/join/probe.hpp | 18 ++++-- data/include/join/sax.hpp | 3 +- data/include/join/value.hpp | 30 ++++----- data/tests/document_test.cpp | 2 +- data/tests/json_reader_test.cpp | 47 ++++++++++++++ data/tests/pack_reader_test.cpp | 42 +++++++++++- data/tests/sax_handler_test.cpp | 2 +- 14 files changed, 258 insertions(+), 86 deletions(-) diff --git a/core/include/join/utils.hpp b/core/include/join/utils.hpp index 49574fb6..7a70d6cd 100644 --- a/core/include/join/utils.hpp +++ b/core/include/join/utils.hpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -66,6 +67,16 @@ namespace join }; } + /** + * @brief throw an exception out of the hot path. + * @param args exception constructor arguments. + */ + template + [[noreturn]] __attribute__ ((cold, noinline)) void throwException (Args&&... args) + { + throw Exception (std::forward (args)...); + } + /** * @brief swaps byte orders. * @param val value to swap. diff --git a/core/include/join/variant.hpp b/core/include/join/variant.hpp index 9d0936c9..8cade182 100644 --- a/core/include/join/variant.hpp +++ b/core/include/join/variant.hpp @@ -27,6 +27,7 @@ // libjoin. #include +#include // C++. #include @@ -36,6 +37,7 @@ // C. #include +#include namespace join { @@ -505,7 +507,7 @@ namespace join { if (index () != find_index_v) { - throw std::bad_cast (); + throwException (); } return *reinterpret_cast (storage ()); } @@ -520,7 +522,7 @@ namespace join { if (index () != find_index_v) { - throw std::bad_cast (); + throwException (); } return *reinterpret_cast (storage ()); } @@ -535,7 +537,7 @@ namespace join { if (index () != I) { - throw std::bad_cast (); + throwException (); } return *reinterpret_cast>*> (storage ()); } @@ -550,7 +552,7 @@ namespace join { if (index () != I) { - throw std::bad_cast (); + throwException (); } return *reinterpret_cast>*> (storage ()); } @@ -621,6 +623,10 @@ namespace join */ constexpr std::size_t index () const noexcept { + if (_which >= sizeof...(Ts)) + { + __builtin_unreachable (); + } return _which; } @@ -710,7 +716,7 @@ namespace join alignas (Ts...) unsigned char _data[std::max ({sizeof (Ts)...})]; /// index of the alternative that is currently held by the variant. - std::size_t _which = 0; + uint32_t _which = 0; }; /** diff --git a/core/tests/utils_test.cpp b/core/tests/utils_test.cpp index 6c6404ad..247d643a 100644 --- a/core/tests/utils_test.cpp +++ b/core/tests/utils_test.cpp @@ -29,6 +29,7 @@ #include // C++. +#include #include #include @@ -365,6 +366,15 @@ TEST (Utils, nextPow2) static_assert (join::isPow2 (join::nextPow2 (300)), "nextPow2 must yield a power of two"); } +/** + * @brief test throwException. + */ +TEST (Utils, throwException) +{ + EXPECT_THROW (join::throwException (), std::bad_cast); + EXPECT_THROW (join::throwException ("invalid key"), std::out_of_range); +} + /** * @brief main function. */ diff --git a/data/include/join/document.hpp b/data/include/join/document.hpp index 63671e3b..7959c2d9 100644 --- a/data/include/join/document.hpp +++ b/data/include/join/document.hpp @@ -31,6 +31,7 @@ #include // C++. +#include #include #include #include @@ -215,19 +216,9 @@ namespace join * @param value string value to set. * @return 0 on success, -1 otherwise. */ - int setString (const std::string& value) + int setString (std::string_view value) { - return emplaceValue (value); - } - - /** - * @brief set string value. - * @param value string value to move. - * @return 0 on success, -1 otherwise. - */ - int setString (std::string&& value) - { - return emplaceValue (std::move (value)); + return emplaceValue (std::string (value)); } /** diff --git a/data/include/join/dtoa.hpp b/data/include/join/dtoa.hpp index e358b1a1..c79309fd 100644 --- a/data/include/join/dtoa.hpp +++ b/data/include/join/dtoa.hpp @@ -71,7 +71,16 @@ namespace join } else if ((0 < kk) && (kk <= 21)) { - memmove (&buffer[kk + 1], &buffer[kk], length - kk); + if ((length - kk <= 16) && (kk <= 7)) + { + char tmp[16]; + memcpy (tmp, &buffer[kk], 16); + memcpy (&buffer[kk + 1], tmp, 16); + } + else + { + memmove (&buffer[kk + 1], &buffer[kk], length - kk); + } buffer[kk] = '.'; return &buffer[length + 1]; } @@ -170,10 +179,8 @@ namespace join delta *= 10; unit *= 10; char d = static_cast (p2 >> -one._exponent); - if (d || length) - { - buffer[length++] = '0' + d; - } + buffer[length] = '0' + d; + length += (d || length); p2 &= one._mantissa - 1; --kappa; if (p2 < delta) diff --git a/data/include/join/json.hpp b/data/include/join/json.hpp index 49376e64..03e25394 100644 --- a/data/include/join/json.hpp +++ b/data/include/join/json.hpp @@ -383,7 +383,7 @@ namespace join * @param value string value to set. * @return 0 on success, -1 otherwise. */ - int setString (const std::string& value) + int setString (std::string_view value) { array (); append ('"'); @@ -433,8 +433,7 @@ namespace join --_depth; if (!_first) { - endLine (); - indent (); + newLine (); } append (']'); endValue (); @@ -489,8 +488,8 @@ namespace join { return !a.first.isString () && b.first.isString (); } - const std::string& sa = a.first.getString (); - const std::string& sb = b.first.getString (); + std::string_view sa = a.first.getString (); + std::string_view sb = b.first.getString (); return utf8::compareUtf16Order (sa.data (), sa.data () + sa.size (), sb.data (), sb.data () + sb.size ()) < 0; }); @@ -517,8 +516,7 @@ namespace join return -1; } comma (); - endLine (); - indent (); + newLine (); append ('"'); if (writeEscaped (key.getString ()) == -1) { @@ -547,8 +545,7 @@ namespace join --_depth; if (!_first) { - endLine (); - indent (); + newLine (); } append ('}'); endValue (); @@ -714,7 +711,7 @@ namespace join * @return 0 on success, -1 otherwise. */ template - int writeEscaped (const std::string& value) + int writeEscaped (std::string_view value) requires (Mode == JsonWriteMode::None) { const char* cur = value.data (); @@ -782,7 +779,7 @@ namespace join * @return 0 on success, -1 otherwise. */ template - int writeEscaped (const std::string& value) + int writeEscaped (std::string_view value) requires (Mode == JsonWriteMode::Canonical) { if (JOIN_UNLIKELY (utf8::validate (value.data (), value.data () + value.size ()) == -1)) @@ -857,28 +854,54 @@ namespace join */ inline void endLine () { - if (_indentation) - { - append ('\n'); - } + append ('\n'); } /** * @brief write indentation. */ inline void indent () + { + static constexpr char spaces[] = " "; + size_t count = _depth * _indentation; + while (count) + { + size_t n = (count < sizeof (spaces) - 1) ? count : sizeof (spaces) - 1; + append (spaces, n); + count -= n; + } + } + + /** + * @brief write end of line and indentation. + */ + inline void newLine () { if (_indentation) { - static constexpr char spaces[] = " "; - size_t count = _depth * _indentation; - while (count) + indentLine (); + } + } + + /** + * @brief write end of line and indentation, out of the compact writing path. + */ + __attribute__ ((noinline)) void indentLine () + { + static constexpr char spaces[] = "\n "; + size_t count = _depth * _indentation; + if (JOIN_LIKELY (count < sizeof (spaces) - 1)) + { + if (JOIN_UNLIKELY (sizeof (_buffer) - _size < sizeof (spaces) - 1)) { - size_t n = (count < sizeof (spaces) - 1) ? count : sizeof (spaces) - 1; - append (spaces, n); - count -= n; + flush (); } + std::memcpy (_buffer + _size, spaces, sizeof (spaces) - 1); + _size += count + 1; + return; } + endLine (); + indent (); } /** @@ -900,8 +923,7 @@ namespace join comma (); if (_depth && ((_arrays[(_depth - 1) >> 6] >> ((_depth - 1) & 63)) & 1)) { - endLine (); - indent (); + newLine (); } } @@ -1832,16 +1854,34 @@ namespace join template int readString (ProbeType& document, Handler& handler, bool isKey = false) { - thread_local Value output{std::string ()}; - output.clear (); - output.reserve (64); + std::string_view chunk; + + if constexpr (requires { document.readUntilEscaped (); }) + { + chunk = document.readUntilEscaped (); + + if (JOIN_UNLIKELY (validateChunk (chunk) == -1)) + { + return -1; + } + + if (JOIN_LIKELY (document.peek () == '"')) + { + document.get (); + return isKey ? handler.setKey (Value (std::string (chunk))) : handler.setString (chunk); + } + } + + thread_local std::string cache; + std::string output = std::move (cache); + output.assign (chunk); for (;;) { - size_t pos = output.getString ().size (); - document.readUntilEscaped (output.getString ()); + size_t pos = output.size (); + document.readUntilEscaped (output); - if (JOIN_UNLIKELY (validateChunk (output.getString (), pos) == -1)) + if (JOIN_UNLIKELY (validateChunk (std::string_view (output).substr (pos)) == -1)) { return -1; } @@ -1857,7 +1897,7 @@ namespace join if (ch == '\\') { document.get (); - if (readEscaped (document, output.getString ()) == -1) + if (readEscaped (document, output) == -1) { return -1; } @@ -1871,7 +1911,9 @@ namespace join } } - return isKey ? handler.setKey (Value (output)) : handler.setString (std::string (output.getString ())); + int result = isKey ? handler.setKey (Value (output)) : handler.setString (output); + cache = std::move (output); + return result; } /** @@ -2064,15 +2106,14 @@ namespace join /** * @brief validate the encoding of the last chunk read, when encoding validation is enabled. - * @param output parse output string. - * @param pos position of the chunk in the output string. + * @param chunk last chunk read. * @return 0 on success, -1 otherwise. */ - inline int validateChunk ([[maybe_unused]] const std::string& output, [[maybe_unused]] size_t pos) noexcept + inline int validateChunk ([[maybe_unused]] std::string_view chunk) noexcept { if constexpr (ReadMode & JsonReadMode::ValidateEncoding) { - if (JOIN_UNLIKELY (utf8::validate (output.data () + pos, output.data () + output.size ()) == -1)) + if (JOIN_UNLIKELY (utf8::validate (chunk.data (), chunk.data () + chunk.size ()) == -1)) { join::lastError = make_error_code (JsonErrc::InvalidEncoding); return -1; diff --git a/data/include/join/pack.hpp b/data/include/join/pack.hpp index 94265476..c83cf422 100644 --- a/data/include/join/pack.hpp +++ b/data/include/join/pack.hpp @@ -309,7 +309,7 @@ namespace join * @param value string value to set. * @return 0 on success, -1 otherwise. */ - int setString (const std::string& value) + int setString (std::string_view value) { if (value.size () < 32) { @@ -330,7 +330,7 @@ namespace join append (0xdb); pack (static_cast (value.size ())); } - append (value.c_str (), static_cast (value.size ())); + append (value.data (), static_cast (value.size ())); endValue (); return 0; } @@ -1055,13 +1055,17 @@ namespace join len = static_cast (document.get ()) & ~0xa0u; } - std::string output; + thread_local std::string cache; + std::string output = std::move (cache); + output.clear (); if (JOIN_UNLIKELY (readBytes (document, len, output) == -1)) { return -1; } - return isKey ? handler.setKey (Value (std::move (output))) : handler.setString (std::move (output)); + int result = isKey ? handler.setKey (Value (output)) : handler.setString (output); + cache = std::move (output); + return result; } /** @@ -1097,13 +1101,17 @@ namespace join } } - std::string output; + thread_local std::string cache; + std::string output = std::move (cache); + output.clear (); if (JOIN_UNLIKELY (readBytes (document, len, output) == -1)) { return -1; } - return handler.setString (std::move (output)); + int result = handler.setString (output); + cache = std::move (output); + return result; } /** diff --git a/data/include/join/probe.hpp b/data/include/join/probe.hpp index 49c34e56..009a3a29 100644 --- a/data/include/join/probe.hpp +++ b/data/include/join/probe.hpp @@ -29,6 +29,7 @@ #include // C++. +#include #include #include #include @@ -261,10 +262,10 @@ namespace join } /** - * @brief read characters until escaped. - * @param out output buffer. + * @brief read characters until escaped, without copying them. + * @return characters read. */ - inline void readUntilEscaped (std::string& out) noexcept + inline std::string_view readUntilEscaped () noexcept { const char* beg = _cur; const char* cur = _cur; @@ -293,8 +294,17 @@ namespace join ++cur; } - out.append (beg, static_cast (cur - beg)); _cur = cur; + return std::string_view (beg, static_cast (cur - beg)); + } + + /** + * @brief read characters until escaped. + * @param out output buffer. + */ + inline void readUntilEscaped (std::string& out) noexcept + { + out.append (readUntilEscaped ()); } /** diff --git a/data/include/join/sax.hpp b/data/include/join/sax.hpp index e7310f69..2d698e32 100644 --- a/data/include/join/sax.hpp +++ b/data/include/join/sax.hpp @@ -31,6 +31,7 @@ // C++. #include +#include #include // C. @@ -174,7 +175,7 @@ namespace join * @param value string value to set. * @return 0. */ - int setString ([[maybe_unused]] const std::string& value) noexcept + int setString ([[maybe_unused]] std::string_view value) noexcept { return 0; } diff --git a/data/include/join/value.hpp b/data/include/join/value.hpp index eea60b90..dbdb3d7f 100644 --- a/data/include/join/value.hpp +++ b/data/include/join/value.hpp @@ -386,7 +386,7 @@ namespace join } } - throw std::bad_cast (); + throwException (); } /** @@ -460,7 +460,7 @@ namespace join } } - throw std::bad_cast (); + throwException (); } /** @@ -535,7 +535,7 @@ namespace join } } - throw std::bad_cast (); + throwException (); } /** @@ -609,7 +609,7 @@ namespace join } } - throw std::bad_cast (); + throwException (); } /** @@ -1112,7 +1112,7 @@ namespace join break; } - throw std::bad_cast (); + throwException (); } /** @@ -1137,7 +1137,7 @@ namespace join break; } - throw std::bad_cast (); + throwException (); } /** @@ -1165,7 +1165,7 @@ namespace join break; } - throw std::bad_cast (); + throwException (); } /** @@ -1192,7 +1192,7 @@ namespace join break; } - throw std::bad_cast (); + throwException (); } /** @@ -1365,7 +1365,7 @@ namespace join break; } - throw std::bad_cast (); + throwException (); } /** @@ -1398,7 +1398,7 @@ namespace join break; } - throw std::bad_cast (); + throwException (); } /** @@ -1425,7 +1425,7 @@ namespace join } } - throw std::bad_cast (); + throwException (); } /** @@ -1452,7 +1452,7 @@ namespace join } } - throw std::bad_cast (); + throwException (); } /** @@ -1479,7 +1479,7 @@ namespace join } } - throw std::bad_cast (); + throwException (); } /** @@ -1506,7 +1506,7 @@ namespace join } } - throw std::bad_cast (); + throwException (); } /** @@ -1533,7 +1533,7 @@ namespace join break; } - throw std::bad_cast (); + throwException (); } /// variant holding the value. diff --git a/data/tests/document_test.cpp b/data/tests/document_test.cpp index 4f0ea22a..440263bb 100644 --- a/data/tests/document_test.cpp +++ b/data/tests/document_test.cpp @@ -113,7 +113,7 @@ struct Counter : public join::SaxHandler return count (); } - int setString (const std::string&) + int setString (std::string_view) { return count (); } diff --git a/data/tests/json_reader_test.cpp b/data/tests/json_reader_test.cpp index 83ac42c0..42c6bed8 100644 --- a/data/tests/json_reader_test.cpp +++ b/data/tests/json_reader_test.cpp @@ -32,6 +32,8 @@ // C++. #include #include +#include +#include // C. #include @@ -69,6 +71,29 @@ struct FailingBuffer : public std::streambuf std::string written; }; +/** + * @brief handler parsing the strings it receives as nested documents. + */ +struct Nested : public join::SaxHandler +{ + int setString (std::string_view value) + { + if (value.starts_with ('[')) + { + Nested inner; + if (JsonReader ().deserialize (value.data (), value.size (), inner) == -1) + { + return -1; + } + strings.insert (strings.end (), inner.strings.begin (), inner.strings.end ()); + } + strings.emplace_back (value); + return 0; + } + + std::vector strings; +}; + /** * @brief Test deserialize method. */ @@ -1443,6 +1468,24 @@ TEST (JsonReader, str) ASSERT_FALSE (value.empty ()); ASSERT_TRUE (value[0].isString ()); EXPECT_STREQ (value[0].getString ().c_str (), "/"); + + ASSERT_EQ (JsonReader ().deserialize (std::string ("[\"Hello\"]"), document), 0) << join::lastError.message (); + EXPECT_STREQ (value[0].getString ().c_str (), "Hello"); + + ASSERT_EQ (JsonReader ().deserialize (std::string ("{\"key\":\"Hello\\nWorld\"}"), document), 0) + << join::lastError.message (); + EXPECT_STREQ (value["key"].getString ().c_str (), "Hello\nWorld"); + + ASSERT_EQ (JsonReader ().deserialize (std::string ("[\"Hello"), document), -1); + + ASSERT_EQ (JsonReader ().deserialize (std::string ("[\"Hello\x01World\"]"), document), -1); + EXPECT_EQ (join::lastError, JsonErrc::IllegalCharacter); + + Nested nested; + ASSERT_EQ (JsonReader ().deserialize (std::string (R"(["[\"x\\ny\"]"])"), nested), 0) << join::lastError.message (); + ASSERT_EQ (nested.strings.size (), 2); + EXPECT_EQ (nested.strings[0], "x\ny"); + EXPECT_EQ (nested.strings[1], R"(["x\ny"])"); } /** @@ -1562,6 +1605,10 @@ TEST (JsonReader, encoding) EXPECT_EQ (join::lastError, JsonErrc::InvalidEncoding); ASSERT_EQ (BasicJsonReader ().deserialize (str.substr (0, 8) + "]", document), 0) << join::lastError.message (); + + ASSERT_EQ (BasicJsonReader ().deserialize (std::string ("{\"\xFF\":1}"), document), + -1); + EXPECT_EQ (join::lastError, JsonErrc::InvalidEncoding); } /** diff --git a/data/tests/pack_reader_test.cpp b/data/tests/pack_reader_test.cpp index b368f635..052f69a2 100644 --- a/data/tests/pack_reader_test.cpp +++ b/data/tests/pack_reader_test.cpp @@ -33,6 +33,8 @@ // C++. #include #include +#include +#include // C. #include @@ -53,12 +55,35 @@ using join::Document; */ struct Throwing : public join::SaxHandler { - int setString (const std::string&) + int setString (std::string_view) { throw std::range_error ("user"); } }; +/** + * @brief handler parsing the strings it receives as nested documents. + */ +struct Nested : public join::SaxHandler +{ + int setString (std::string_view value) + { + if (value.starts_with ('\x91')) + { + Nested inner; + if (PackReader ().deserialize (value.data (), value.size (), inner) == -1) + { + return -1; + } + strings.insert (strings.end (), inner.strings.begin (), inner.strings.end ()); + } + strings.emplace_back (value); + return 0; + } + + std::vector strings; +}; + /** * @brief stream buffer failing on its first write. */ @@ -1306,6 +1331,13 @@ TEST (PackReader, str) ASSERT_FALSE (value.empty ()); ASSERT_TRUE (value[0].isString ()); EXPECT_STREQ (value[0].getString ().c_str (), "\xF0\x9D\x84\x9E"); + + Nested nested; + ASSERT_EQ (PackReader ().deserialize (std::string ({'\x91', '\xa4', '\x91', '\xa2', 'a', 'b'}), nested), 0) + << join::lastError.message (); + ASSERT_EQ (nested.strings.size (), 2); + EXPECT_EQ (nested.strings[0], "ab"); + EXPECT_EQ (nested.strings[1], std::string ({'\x91', '\xa2', 'a', 'b'})); } /** @@ -1393,6 +1425,14 @@ TEST (PackReader, bin) ASSERT_FALSE (value.empty ()); ASSERT_TRUE (value[0].isString ()); EXPECT_STREQ (value[0].getString ().c_str (), "\xF0\x9D\x84\x9E"); + + Nested nested; + ASSERT_EQ ( + PackReader ().deserialize (std::string ({'\x91', '\xc4', '\x05', '\x91', '\xc4', '\x02', 'a', 'b'}), nested), 0) + << join::lastError.message (); + ASSERT_EQ (nested.strings.size (), 2); + EXPECT_EQ (nested.strings[0], "ab"); + EXPECT_EQ (nested.strings[1], std::string ({'\x91', '\xc4', '\x02', 'a', 'b'})); } /** diff --git a/data/tests/sax_handler_test.cpp b/data/tests/sax_handler_test.cpp index a29d11ab..e21e14d8 100644 --- a/data/tests/sax_handler_test.cpp +++ b/data/tests/sax_handler_test.cpp @@ -43,7 +43,7 @@ using join::JsonReader; */ struct StringCounter : public SaxHandler { - int setString (const std::string&) + int setString (std::string_view) { ++strings; return 0;