nlohmann / nlohmann/json

Idea: arena / PMR document mode (O(1) teardown, allocation-free destruction)

Open
#5,298 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C++
Stars
50.6k
Forks
7.5k
Avg merge
4d 17h
Merged PRs (30d)
58

Description

Status: brainstorming, not a plan

This is an idea to move forward on, not a sanctioned TODO or a commitment. It is a hypothesis to investigate, measure, and discuss. Treat this as a starting point for discussion.

Motivation

This is really the general cure for a cluster of already-tracked symptoms, reframed as one design rather than a new feature:

  • #5135 — destructor allocates, violating noexcept semantics
  • #3583 — basic_json destructor is slow
  • #5239 — allocation failure during JSON destruction
  • #4843 — use the provided allocator in destroy()

All four have the same root cause: there is no arena, so teardown is a recursive walk that frees every node individually. With a monotonic arena, teardown is O(1) and allocation-free by construction.

Why PMR is blocked today — a one-line diagnosis

create<T>() at include/nlohmann/json.hpp:411 does AllocatorType<T> alloc; — it default-constructs a fresh allocator on every allocation, and the destroy sites (include/nlohmann/json.hpp:652+, :2526+, :2597+) do the same. That hard-codes the assumption that the allocator is stateless.

std::pmr::polymorphic_allocator is stateful — it holds a memory_resource*. Under the current code every node would default-construct a pmr allocator pointing at the default resource (ignoring the intended arena), and teardown would deallocate through a default-constructed allocator, which for pmr is undefined behavior (deallocating on the wrong resource). No allocator instance is stored anywhere in basic_json, and nothing propagates one to children.

Two ways forward

(a) Full stateful-allocator support — probably not worth it

Store the allocator in each node, honor allocator_traits POCCA/POCMA/select_on_container_copy_construction, propagate on copy/move. "Correct," but deeply invasive and it grows sizeof(basic_json) by a pointer per node (the union is currently ~8 bytes + a type byte; +8 bytes everywhere is a real regression for large trees).

(b) Arena / PMR document mode — the Boost.JSON model (recommended)
  • Store the memory resource once at the root (a storage_ptr-style handle), not per node.
  • Thread it through parse() so all of a document's nodes come from one std::pmr::monotonic_buffer_resource.
  • Crucially, tear the document down by dropping the arena, not by running per-node destructors. This needs a "no individual free" destruction path (skip the deallocate loop when the tree is arena-owned) — the part that's missing today.

This directly resolves #5135 / #3583 / #5239 / #4843: teardown becomes O(1) and cannot allocate or throw.

Sketch

std::pmr::monotonic_buffer_resource arena;
auto doc = json::parse_into(arena, src);   // illustrative: all nodes from `arena`
// ... use doc ...
// destruction: drop `arena`; no per-node frees run

Implementation notes

  • Add a storage_ptr-like handle (non-owning or ref-counted pointer to a memory_resource) stored once at the document root; children reference it.
  • Route create<T>() and every destroy site through the document's resource instead of a default-constructed allocator. These sites are load-bearing and must all be updated consistently.
  • Add an arena-owned flag / teardown path that skips per-node deallocation.
  • Keep the change opt-in: default behavior and sizeof(basic_json) must be unchanged for existing users.

Open questions / pitfalls

  • Copying an arena-backed value out of its document must deep-copy into the destination's allocator — arena-owned storage cannot be shared across trees.
  • select_on_container_copy_construction-correct behavior at the document boundary, even if individual nodes stay allocator-light.
  • Move semantics across documents with different resources.
  • How the public API expresses "this document owns/references arena X" without leaking lifetime footguns (the arena must outlive the document).
  • Interaction with JSON_NO_IO/binary paths and with the borrowed-view idea (#5295), which is a different way to avoid per-node allocation (references source bytes rather than arena-allocating owned nodes).

Dependencies / interactions

  • Subsumes the destructor-cost issues #5135, #3583, #5239, #4843 — best treated as their shared design rather than four separate fixes.
  • Independent of the lossless-number (#5296) and canonical-serialization (#5297) ideas.
  • Complementary to, but distinct from, the borrowed-view idea (#5295): arena mode still produces an owning, mutable DOM; the view does not.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with include/nlohmann/json.hpp at create() around line 411 and the destroy sites around lines 652, 2526, and 2597; trace how allocators are constructed and propagated. Investigate the arena/document boundary, copying and moving between resources, and the JSON_NO_IO, binary, and borrowed-view interactions. Done should be a measured, opt-in design that makes arena-backed teardown O(1) and allocation-free while preserving default behavior and sizeof(basic_json).

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
performance
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.