Idea: Decode into an existing value and reuse its storage (get_into)
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
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_jsonfor sequence containers (from_json_array_impl) builds a new container, decodes each element withget<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 newstring_tthat is move-assigned. Even a plainstd::stringmember 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 asget_to()does.- Strings: assigned into the existing buffer. For
basic_jsonthis already happens; for views it becomess.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-writtenfrom_jsonthat 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>>, andvector<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, whereItemis 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_documentand decode fromroot().
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 onget_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, andstd::optionalare not covered yet. - User types: should there be a customization point (
adl_serializer::from_json_intoor 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
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de nlohmann/json
-
documentation kind: bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
Los mantenedores suelen responder en 1 día
-
kind: bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
Los mantenedores suelen responder en 1 día
-
kind: bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
Los mantenedores suelen responder en 1 día
-
kind: bug performance
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
Los mantenedores suelen responder en 1 día
-
dump(): the recursive frame grew from 480 to 2304 bytes in unoptimized builds, so 128 levels need 288 KiB of stackPosiblemente ocupada @nlohmann la tomó hoy. Abiertokind: enhancement/improvement
nlohmann/json#6116 · 1 asignado ·
Los mantenedores suelen responder en 1 día
Todos los issues de nlohmann/json
Issues similares
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 62/100
bazel-contrib/rules_go#4784 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
Los mantenedores suelen responder en 1 día
-
TUIs: Fix TUI CMakesAbiertobug
Dificultad 2/5 1-3 horas Aptitud para principiantes 62/100
RMIT-Rover-Team/equinox-2#145 ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 62/100
eunomia-bpf/bpftime#779 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 79/100
Los mantenedores suelen responder en 1 día