Idea: Decode into an existing value and reuse its storage (get_into)

Abierto
#6,027 3 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
5/5
Tiempo estimado
Más de una semana
Aptitud para principiantes
35/100
Tipo de issue
Nueva funcionalidad
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
cpp
Área
backend

Línea de trabajo

Read the proposal’s vector decoding example and its equivalence checks, then inspect the existing get_to() and from_json paths for sequence containers and the NLOHMANN_DEFINE_TYPE_* macros. The issue describes a prototype outside the library, but names no repository files or tests to run. Done means agreeing on the API and exception guarantee, then implementing reuse for the stated types while matching get<T>() values and exception messages.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

Idea

Code that decodes a stream of messages into the same C++ value again and again, such as a request loop or a polling client, pays for new allocations on every call. get_to() throws away the storage the target already owns:

  • from_json for sequence containers (from_json_array_impl) builds a new container, decodes each element with get<value_type>() and move-assigns the result. The target's capacity and every element's own buffers are lost.
  • From a json_view, string_value(View) returns a new string_t that is move-assigned. Even a plain std::string member never reuses its buffer.

Go's encoding/json/v2 (blog post) instead decodes into the existing value and reuses its slices and strings. The same idea fits here as an opt-in call, without changing what get_to() does.

Proposal

A new call that gives v the same value as v = j.get<T>(), but reuses v's storage:

j.get_into(v);   // or a free function; the name is open

It accepts basic_json and json_view input.

  • std::vector<E>: existing elements are decoded in place, missing ones appended, surplus ones erased. Capacity is reserved as get_to() does.
  • Strings: assigned into the existing buffer. For basic_json this already happens; for views it becomes s.assign(data, size).
  • Types from the NLOHMANN_DEFINE_TYPE_* macros: each member is decoded in place, recursively. The macros would generate one extra function per type for this.
  • Everything else gets a freshly decoded value assigned (e = elem.get<E>()). A hand-written from_json that leaves fields alone when a key is missing therefore never sees stale data from the previous value.
  • Excluded: std::vector<bool> (proxy references).
  • A vector with no capacity goes straight to get_to(), so decoding into a new value runs exactly today's code.

An element is decoded in place only if it is arithmetic, string_t, a reusable vector, or a macro type. That rule is what keeps the result identical to get<T>().

Prototype

The prototype is a header outside the library (about 150 lines), plus a variant of NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE that also generates the in-place decoder. Core of the vector case:

template<class J> static void go(const J& j, std::vector<E, A>& v)
{
    if (!j.is_array() || v.capacity() == 0)
    {
        j.get_to(v); // nothing to reuse, or throws exactly as get_to()
        return;
    }
    if (v.capacity() < j.size())
    {
        v.reserve(j.size());
    }
    const std::size_t old_size = v.size();
    std::size_t i = 0;
    for (const auto& elem : elements(j)) // array_t for basic_json, the view itself for views
    {
        if (i < old_size)
        {
            update(elem, v[i], in_place_ok<J, E> {}); // reuse_into(elem, v[i]) or v[i] = elem.get<E>()
        }
        else
        {
            v.push_back(elem.template get<E>());
        }
        ++i;
    }
    v.erase(v.begin() + static_cast<std::ptrdiff_t>((std::min)(i, v.size())), v.end());
}

Checks: an equivalence check compares get_into() on a prefilled target with get<T>(), both values and exception messages. Inputs:

  • 31×31 pairs of old and new batches, including empty and 5-record batches;
  • broken input: a wrong type nested inside, a missing member, an object instead of an array, an extra element of the wrong type, and null/string/object at the top level;
  • a hand-written partial from_json, vector<vector<string>>, and vector<bool>.

It passes for basic_json and json_document views, in C++11 and C++17, with JSON_DIAGNOSTICS 0 and 1, under ASan/UBSan. Two deliberately broken variants show that the check catches errors: decoding a hand-written type in place (stale field, 1 mismatch) and a broken view string assignment (901 mismatches).

Benchmark

Test data: batches of Record values, each with a 7-member struct:

  • two strings;
  • a vector<string> of 3–6 tags;
  • a vector<int> of 12–20 values;
  • a vector<Item> of 2–5 items, where Item is a 3-member struct;
  • a double and a bool.

All strings are longer than 15 characters, so they are heap-allocated; this favours reuse. Batches hold 1 or about 100 records (±10 %), and the target is decoded from 20 different batches in turn.

Phases:

  • same target: decode into a target that holds the previous batch;
  • parse + decode: the same, including parsing the text;
  • document: parse into a json_document and decode from root().
Apple M1 Max, Apple clang 21, -O3

Instructions and cycles, median of 5 runs with setup subtracted; runs spread by under 2 %. Each cell is the change for 1 / ~100 records.

comparison instructions cycles
tree: get_into vs get_to, same target −44 / −41 % −34 / −29 %
tree: parse + get_into vs parse + get_to −8 / −8 % −7 / −8 %
view: get_into vs get_to, same target −44 / −41 % −44 / −37 %
document parse + get_into vs document parse + get_to −27 / −25 % −25 / −24 %
document parse + get_to vs tree parse + get_to −71 / −72 % −73 / −72 %
document parse + get_into vs tree parse + get_to −79 / −79 % −80 / −78 %
x86-64 Haswell VPS, GCC 15.2 and clang 21, -O3, pinned

Time per batch; the VPS is shared, so runs spread by up to 18 %. Builds aligned with -falign-functions=64 -falign-loops=64 (GCC) and -mllvm -align-all-functions=6 (clang) agree.

comparison GCC 15.2 clang 21
tree: get_into vs get_to, same target −25 / −49 % −21 / −40 %
tree: parse + get_into vs parse + get_to −10 / −7 % −8 / −9 %
view: get_into vs get_to, same target −27 / −55 % −24 / −50 %
document parse + get_into vs document parse + get_to −14 / −35 % −11 / −34 %
document parse + get_to vs tree parse + get_to −73 / −74 % −74 / −75 %
document parse + get_into vs tree parse + get_to −77 / −83 % −76 / −83 %

For scale, with ~100-record batches on the VPS (GCC), one record takes 7.06 µs with parse() + get_to(), 1.85 µs with a json_document, and 1.19 µs with a json_document plus get_into().

Decoding into an empty target runs the same code as get_to(): +0.0–0.2 % instructions on M1, within noise on the VPS.

With a tree, parsing dominates, so reuse saves 7–10 % end to end. With a json_document, decoding is a larger share of the work and reuse saves 11–35 %.

A smaller change on its own

get_to() from a view into a std::string could assign into the existing buffer (s.assign(r.data(), r.size())) instead of building a new string. This helps today's get_to() from a view into an existing struct, needs no new API, and keeps the strong exception guarantee for strings.

Behaviour differences

  • Exception guarantee: basic instead of strong. If decoding throws, the target may be partly overwritten. get_to() for vectors leaves the target unchanged today.
  • Otherwise none intended: the result and exception messages are the same as get<T>(), and exceptions are thrown in the same order.

Breaking changes

None. get_to() and get() keep their behaviour; get_into() and the extra function the macros generate are additions. The macro change only adds a function, so user types defined with the macros keep compiling.

Open questions

  • API: a member get_into(), a free function, or an option on get_to()? Is the weaker exception guarantee acceptable for a call whose name says it reuses the target?
  • Containers: maps (via node extract()/reinsert), std::deque, std::array, and std::optional are not covered yet.
  • User types: should there be a customization point (adl_serializer::from_json_into or similar) so hand-written types can opt in to in-place decoding?
  • Macros: extend to all NLOHMANN_DEFINE_TYPE_* variants; combine with the one-walk lookup of #6026 so the in-place decoder also walks each object once.
  • Data: with short strings (SSO) the gain will be smaller. Is there a realistic dataset to measure on?

🤖 Generated with Claude Code

Lenguaje dominante
C++
Estrellas
50.9k
Forks
7.5k
Merge medio
1 d 3 h
PR fusionados (30 d)
217

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de nlohmann/json

Todos los issues de nlohmann/json

Issues similares

Más issues de C++

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.