mirror of
https://github.com/nlohmann/json.git
synced 2026-07-30 07:33:01 +04:00
Buffer serializer output and add ensure_ascii string fast path
Two further serialization speedups on top of the ensure_ascii=false bulk
copy, both reusing the SWAR primitives in detail/input/string_scan.hpp.
1. Internal write buffer (devirtualization). Every structural character
('{', '"', ',', ...) previously went straight to the output adapter
through a virtual call. Route all writes through put_char/put_chars
into a 1 KiB buffer that flushes in bulk; the public dump() flushes
once the top-level value is done (the recursive worker is split out as
dump_internal). Runs larger than the buffer are written straight
through, so large payloads are not copied twice. This is the dominant
cost for object/array-heavy values.
2. ensure_ascii fast path. dump_escaped previously ran the UTF-8 DFA over
every byte when escaping non-ASCII. Add find_ascii_copyable_run() (a
SWAR scan stopping at '"', '\\', < 0x20, 0x7F, and >= 0x80) so runs of
printable ASCII are bulk-copied, with the byte path handling each
escape/non-ASCII byte exactly as before.
Behavior is unchanged: dump output is byte-for-byte identical to the
previous implementation across ~20k randomized byte strings plus curated
edge cases (all escapes, control chars, 0x7F, valid multibyte,
surrogates, overlong, truncated), for object/array/pretty output, both
ensure_ascii settings, and all three error handlers, in C++11/17/20 at
-O2/-O3. New unit tests cover the buffer flush boundaries, the escape and
0x7F handling, multibyte under both settings, and invalid-UTF-8 handling.
Throughput (g++ -O3, vs the ensure_ascii=false-only baseline):
long ASCII, ensure_ascii=0 4.2x
long ASCII, ensure_ascii=1 4.1x
twitter-like objects 2.7x
dense CJK 1.8x
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAYM1qhSA2FDaDcGfPW3fG
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
@@ -382,3 +382,87 @@ TEST_CASE("dump for basic_json with long double number_float_t")
|
||||
check_same(100.0L, 100.0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("serialization of strings (bulk fast path)")
|
||||
{
|
||||
// These cases exercise the SWAR bulk-copy fast path in dump_escaped and the
|
||||
// internal write buffer: long runs, escapes interrupting runs, 0x7F/DEL,
|
||||
// multibyte UTF-8 under both ensure_ascii settings, and payloads larger than
|
||||
// the write buffer.
|
||||
|
||||
SECTION("long unescaped ASCII exceeds the write buffer")
|
||||
{
|
||||
const std::string big(3000, 'a');
|
||||
const json j = big;
|
||||
CHECK(j.dump() == '"' + big + '"');
|
||||
CHECK(j.dump(-1, ' ', true) == '"' + big + '"');
|
||||
// round-trips
|
||||
CHECK(json::parse(j.dump()) == j);
|
||||
}
|
||||
|
||||
SECTION("runs interrupted by escapes")
|
||||
{
|
||||
const json j = std::string(500, 'x') + "\n\"\\" + std::string(500, 'y');
|
||||
const std::string out = j.dump();
|
||||
CHECK(out == '"' + std::string(500, 'x') + "\\n\\\"\\\\" + std::string(500, 'y') + '"');
|
||||
CHECK(json::parse(out) == j);
|
||||
}
|
||||
|
||||
SECTION("DEL (0x7F) depends on ensure_ascii")
|
||||
{
|
||||
const json j = std::string("a\x7f" "b");
|
||||
CHECK(j.dump(-1, ' ', false) == "\"a\x7f" "b\""); // copied verbatim
|
||||
CHECK(j.dump(-1, ' ', true) == "\"a\\u007fb\""); // escaped
|
||||
}
|
||||
|
||||
SECTION("multibyte UTF-8 under both ensure_ascii settings")
|
||||
{
|
||||
const json j = std::string("A\xc3\xa9\xe4\xbd\xa0\xf0\x9f\x98\x80Z"); // A é 你 😀 Z
|
||||
// not escaping non-ASCII: bytes are copied through the bulk validator
|
||||
CHECK(j.dump(-1, ' ', false) == "\"A\xc3\xa9\xe4\xbd\xa0\xf0\x9f\x98\x80Z\"");
|
||||
// ensure_ascii: escaped (with a surrogate pair for the emoji)
|
||||
CHECK(j.dump(-1, ' ', true) == "\"A\\u00e9\\u4f60\\ud83d\\ude00Z\"");
|
||||
CHECK(json::parse(j.dump(-1, ' ', true)) == j);
|
||||
}
|
||||
|
||||
SECTION("many small structural writes exceed the write buffer")
|
||||
{
|
||||
json arr = json::array();
|
||||
for (int i = 0; i < 2000; ++i)
|
||||
{
|
||||
arr.push_back(i);
|
||||
}
|
||||
const std::string out = arr.dump();
|
||||
CHECK(out.front() == '[');
|
||||
CHECK(out.back() == ']');
|
||||
CHECK(json::parse(out) == arr);
|
||||
|
||||
json obj = json::object();
|
||||
for (int i = 0; i < 500; ++i)
|
||||
{
|
||||
obj["key" + std::to_string(i)] = i;
|
||||
}
|
||||
CHECK(json::parse(obj.dump()) == obj);
|
||||
CHECK(json::parse(obj.dump(2)) == obj);
|
||||
|
||||
// deep nesting emits >1024 consecutive single-character writes, forcing
|
||||
// the write buffer to flush mid-run
|
||||
json nested = json::array();
|
||||
for (int i = 0; i < 1100; ++i)
|
||||
{
|
||||
nested = json::array({nested});
|
||||
}
|
||||
const std::string out2 = nested.dump();
|
||||
CHECK(out2.substr(0, 1100) == std::string(1100, '['));
|
||||
CHECK(json::parse(out2) == nested);
|
||||
}
|
||||
|
||||
SECTION("invalid UTF-8 handling is unaffected by the fast path")
|
||||
{
|
||||
const json j = std::string("valid\xff" "more");
|
||||
CHECK_THROWS_WITH_AS(j.dump(), "[json.exception.type_error.316] invalid UTF-8 byte at index 5: 0xFF", json::type_error&);
|
||||
CHECK(j.dump(-1, ' ', false, json::error_handler_t::replace) == "\"valid\xef\xbf\xbd" "more\"");
|
||||
CHECK(j.dump(-1, ' ', true, json::error_handler_t::replace) == "\"valid\\ufffdmore\"");
|
||||
CHECK(j.dump(-1, ' ', false, json::error_handler_t::ignore) == "\"validmore\"");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user