to_bson() is O(size × nesting depth), not linear as documented
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 50.6k
- Forks
- 7.5k
- Avg merge
- 4d 17h
- Merged PRs (30d)
- 58
Description
Description
BSON documents are length-prefixed, so write_bson_object (include/nlohmann/detail/output/binary_writer.hpp:1285) calls calc_bson_object_size to compute the prefix — which recursively walks the entire subtree. It then recurses into each child, and each child walks its subtree again. write_bson_array (binary_writer.hpp:1141 / calc_bson_array_size at :1118) does the same.
The result is that every nesting level re-measures everything below it, giving O(size × depth) instead of O(size).
docs/mkdocs/docs/api/basic_json/to_bson.md currently states:
Complexity
Linear in the size of the JSON value
j.
which is not correct as written.
Reproduction steps
Serialize objects with a roughly constant payload but increasing nesting depth and time to_bson.
Minimal code example
#include <nlohmann/json.hpp>
#include <chrono>
#include <iostream>
using json = nlohmann::json;
int main()
{
for (int depth : {200, 400, 800, 1600})
{
json j = json::object();
json* p = &j;
for (int i = 0; i < depth; ++i)
{
(*p)["k"] = json::object();
p = &(*p)["k"];
}
for (int i = 0; i < 200; ++i)
{
(*p)[std::to_string(i)] = i;
}
const auto t0 = std::chrono::steady_clock::now();
const auto v = json::to_bson(j);
const auto t1 = std::chrono::steady_clock::now();
std::cout << "depth " << depth << " bytes " << v.size() << " time "
<< std::chrono::duration<double, std::milli>(t1 - t0).count() << " ms\n";
}
}
Expected vs. actual results
Expected: runtime roughly proportional to output size.
Actual: runtime grows quadratically in depth — 27× the time for 4.4× the output:
depth 200 bytes 3295 time 1.35 ms
depth 400 bytes 4895 time 3.50 ms
depth 800 bytes 8095 time 10.08 ms
depth 1600 bytes 14495 time 36.04 ms
36 ms to produce 14 KB of BSON is a poor cost for the format, and it makes to_bson() a cheap amplification target when serializing attacker-shaped documents.
Suggested fix
Either:
- memoize sizes in a single bottom-up pass, or
- use the usual patch-the-prefix approach (reserve 4 bytes, write children, seek back and fill in the length) where the output sink supports it.
If neither is desirable, the complexity statement in the to_bson docs should at least be corrected.
Notes
Not addressed by #5286, which is a constant-factor speedup (devirtualized output sink + byte-swap number encoding) and is explicitly output- and algorithm-preserving. write_bson_array also builds each index string twice (std::to_string in both the calc and write passes), which would fold into the same fix.
Compiler and operating system
g++ 13.3.0 (Ubuntu 24.04), -std=c++11 -O2, Linux x86-64
Library version
develop @ 06ac77f
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with write_bson_object and write_bson_array in include/nlohmann/detail/output/binary_writer.hpp, along with their calc_bson_*_size helpers, to trace the repeated subtree walks and index-string construction. Run the supplied nested-document reproduction to confirm the scaling. Done means the BSON serialization complexity is addressed, with docs/mkdocs/docs/api/basic_json/to_bson.md matching the resulting behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- data
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100