From b4fdf765e16c4d6910a0f7469a9c6328c52a473b Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 19 Aug 2026 23:03:15 +0200 Subject: [PATCH] Fill the indentation buffer once instead of once per flush @gregmarr's point on the fill-and-flush loop: flushing does not disturb what the write buffer holds, so an indentation spanning several buffer-fulls only has to be written into the buffer once and can then be handed to the adapter as many times as needed. The loop re-filled it every time, doing work it already knew was there. put_indent() now fills the room left in the buffer, and if anything remains, flushes, fills the buffer once, and re-flushes that same content. It also returns early for a zero-width indentation, which is what the closing brace of every outermost value asks for. Measured over a dump(), counting memset calls and bytes inside put_indent: indent before after 4 1 call / 4 B 1 call / 4 B 2000 2 calls / 2000 B 2 calls / 2046 B 100000 98 calls / 100000 B 2 calls / 2046 B The wide case is now constant work rather than proportional to the indentation width; ordinary widths are unchanged. Tests extended to cover several whole buffer-fulls and an exact multiple of the buffer size. Signed-off-by: Niels Lohmann --- include/nlohmann/detail/output/serializer.hpp | 54 ++++++++++++++----- single_include/nlohmann/json.hpp | 54 ++++++++++++++----- tests/src/unit-serialization.cpp | 6 +++ 3 files changed, 88 insertions(+), 26 deletions(-) diff --git a/include/nlohmann/detail/output/serializer.hpp b/include/nlohmann/detail/output/serializer.hpp index 7cdf85b6c..d618a9ebe 100644 --- a/include/nlohmann/detail/output/serializer.hpp +++ b/include/nlohmann/detail/output/serializer.hpp @@ -718,24 +718,52 @@ class serializer Writes the indentation straight into the buffer instead of copying it out of a pre-grown indentation string, so no auxiliary string has to be sized, - resized, or kept in sync with the deepest nesting level reached. An - indentation wider than the buffer simply fills and flushes it repeatedly. + resized, or kept in sync with the deepest nesting level reached. + + An indentation wider than the buffer is emitted by filling the buffer with + the indentation character once and flushing that same content repeatedly: + flushing does not disturb what the buffer holds, so re-filling it between + flushes would be redundant work. */ void put_indent(unsigned int indent) { - while (indent > 0) + // closing braces at the outermost level ask for no indentation at all + if (indent == 0) { - if (JSON_HEDLEY_UNLIKELY(write_buffer_pos == write_buffer.size())) - { - flush(); - } - - const std::size_t chunk = (std::min)(static_cast(indent), - write_buffer.size() - write_buffer_pos); - std::memset(write_buffer.data() + write_buffer_pos, indent_char, chunk); - write_buffer_pos += chunk; - indent -= static_cast(chunk); + return; } + + const std::size_t capacity = write_buffer.size(); + + // fill whatever room is left in the buffer; this is the whole job + // whenever the indentation is narrower than the buffer, which is the + // case for every sane indent_step + const std::size_t head = (std::min)(static_cast(indent), capacity - write_buffer_pos); + std::memset(write_buffer.data() + write_buffer_pos, indent_char, head); + write_buffer_pos += head; + indent -= static_cast(head); + + if (JSON_HEDLEY_LIKELY(indent == 0)) + { + return; + } + + // the buffer is full and the remainder spans whole buffer-fulls: flush + // what is pending, then fill the buffer with the indentation character + // exactly once and hand the same bytes to the adapter as often as needed + flush(); + std::memset(write_buffer.data(), indent_char, capacity); + + while (indent >= capacity) + { + write_buffer_pos = capacity; + flush(); + indent -= static_cast(capacity); + } + + // the buffer still holds indentation characters throughout, so the tail + // only has to be claimed, not written again + write_buffer_pos = indent; } /*! diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 4f11e2b98..6ead49fc3 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -21599,24 +21599,52 @@ class serializer Writes the indentation straight into the buffer instead of copying it out of a pre-grown indentation string, so no auxiliary string has to be sized, - resized, or kept in sync with the deepest nesting level reached. An - indentation wider than the buffer simply fills and flushes it repeatedly. + resized, or kept in sync with the deepest nesting level reached. + + An indentation wider than the buffer is emitted by filling the buffer with + the indentation character once and flushing that same content repeatedly: + flushing does not disturb what the buffer holds, so re-filling it between + flushes would be redundant work. */ void put_indent(unsigned int indent) { - while (indent > 0) + // closing braces at the outermost level ask for no indentation at all + if (indent == 0) { - if (JSON_HEDLEY_UNLIKELY(write_buffer_pos == write_buffer.size())) - { - flush(); - } - - const std::size_t chunk = (std::min)(static_cast(indent), - write_buffer.size() - write_buffer_pos); - std::memset(write_buffer.data() + write_buffer_pos, indent_char, chunk); - write_buffer_pos += chunk; - indent -= static_cast(chunk); + return; } + + const std::size_t capacity = write_buffer.size(); + + // fill whatever room is left in the buffer; this is the whole job + // whenever the indentation is narrower than the buffer, which is the + // case for every sane indent_step + const std::size_t head = (std::min)(static_cast(indent), capacity - write_buffer_pos); + std::memset(write_buffer.data() + write_buffer_pos, indent_char, head); + write_buffer_pos += head; + indent -= static_cast(head); + + if (JSON_HEDLEY_LIKELY(indent == 0)) + { + return; + } + + // the buffer is full and the remainder spans whole buffer-fulls: flush + // what is pending, then fill the buffer with the indentation character + // exactly once and hand the same bytes to the adapter as often as needed + flush(); + std::memset(write_buffer.data(), indent_char, capacity); + + while (indent >= capacity) + { + write_buffer_pos = capacity; + flush(); + indent -= static_cast(capacity); + } + + // the buffer still holds indentation characters throughout, so the tail + // only has to be claimed, not written again + write_buffer_pos = indent; } /*! diff --git a/tests/src/unit-serialization.cpp b/tests/src/unit-serialization.cpp index 504044278..c565dcab9 100644 --- a/tests/src/unit-serialization.cpp +++ b/tests/src/unit-serialization.cpp @@ -485,6 +485,12 @@ TEST_CASE("indentation is written straight into the write buffer") // 2000 > the 1024-byte write buffer, and > the 512 the indentation // string used to start at CHECK(j.dump(2000) == "{\n" + std::string(2000, ' ') + "\"a\": 1\n}"); + // several whole buffer-fulls, so the buffer is refilled once and then + // flushed repeatedly + CHECK(j.dump(5000) == "{\n" + std::string(5000, ' ') + "\"a\": 1\n}"); + CHECK(j.dump(5000, '\t') == "{\n" + std::string(5000, '\t') + "\"a\": 1\n}"); + // an exact multiple of the buffer size + CHECK(j.dump(4096) == "{\n" + std::string(4096, ' ') + "\"a\": 1\n}"); } SECTION("a non-space indentation character is used throughout")