Skip to content

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

Description

@nlohmann

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:

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 (Idea: borrowed / zero-copy read-only view over a source buffer #5295), which is a different way to avoid per-node allocation (references source bytes rather than arena-allocating owned nodes).

Dependencies / interactions

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions