Discriminate json_number's three numeric slots into one union

json_number used to carry three parallel 8-byte fields (a double plus
both a 64-bit unsigned and a 64-bit signed slot for the large-integer
cases) even though at most one of the integer slots is ever the
canonical value for any given number. Collapse them into a
discriminated union:

    enum repr_t { REPR_DOUBLE, REPR_LARGE_UNSIGNED, REPR_LARGE_SIGNED };
    repr_t repr;
    union { double d; unsigned long long u; long long s; } value;

Callers keep the same read API: number() returns the appropriate
double, large_unsigned() returns the ull (or 0 if not currently stored
that way), large_signed() likewise. Writes go through new set_number /
set_large_unsigned / set_large_signed methods that keep the
discriminator and the union value in sync.

This was prompted by an observation that moving json_type to the end
of the object should shrink things via tail-padding reuse. Empirically
the type-at-end rearrangement saves nothing on its own (every
subclass payload is 8-byte aligned so it can't slot into the 4-byte
tail), but the discriminated-number redesign hits the same idea from
a different direction: adding the 4-byte `repr` to json_number makes
the class non-standard-layout, which lets the Itanium ABI pack `repr`
into the base's 4-byte tail padding at offset 20. The union value
then starts at the natural offset 24, and json_number ends at offset
32 -- a 33% reduction.

Per-node sizes:
  json_object (TRUE/FALSE/NULL)  24 bytes
  json_number                    32 bytes  (was 48)
  json_string                    48 bytes
  json_array                     48 bytes
  json_hash                      48 bytes

Numbers dominate real GeoJSON (every coordinate is one), so the net
memory win on a typical parse is substantial.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Erica Fischer
2026-05-30 17:49:24 -07:00
co-authored by Cursor
parent bd90f0b4fe
commit 1bf18d39ce
3 changed files with 58 additions and 29 deletions
+6 -7
View File
@@ -482,27 +482,26 @@ again:
json_object_ptr n = add_object(j, JSON_NUMBER);
if (n != nullptr) {
n->number() = atof(j->number_buffer.c_str());
n->large_signed() = 0;
n->large_unsigned() = 0;
double d = atof(j->number_buffer.c_str());
n->set_number(d);
#define MAX_SAFE_INTEGER 9007199254740991.0
#define MIN_SAFE_INTEGER -9007199254740991.0
if (!decimal && n->number() > MAX_SAFE_INTEGER) {
if (!decimal && d > MAX_SAFE_INTEGER) {
errno = 0;
char *err = nullptr;
unsigned long long ull = strtoull(j->number_buffer.c_str(), &err, 10);
if (errno == 0 && (err == nullptr || *err == '\0')) {
n->large_unsigned() = ull;
n->set_large_unsigned(ull);
}
}
if (!decimal && n->number() < MIN_SAFE_INTEGER) {
if (!decimal && d < MIN_SAFE_INTEGER) {
errno = 0;
char *err = nullptr;
long long ll = strtoll(j->number_buffer.c_str(), &err, 10);
if (errno == 0 && (err == nullptr || *err == '\0')) {
n->large_signed() = ll;
n->set_large_signed(ll);
}
}
}