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
+51 -21
View File
@@ -82,12 +82,17 @@ struct json_object {
inline std::string &string();
inline const std::string &string() const;
inline double &number();
// Numbers are stored in a discriminated union (double / unsigned /
// signed) so a json_number is only 40 bytes instead of 48. The
// large_*() accessors return 0 when the number is not currently
// stored in that representation, matching the prior convention
// where "0" meant "not set, fall through to the next slot".
inline double number() const;
inline unsigned long long &large_unsigned();
inline unsigned long long large_unsigned() const;
inline long long &large_signed();
inline long long large_signed() const;
inline void set_number(double d);
inline void set_large_unsigned(unsigned long long u);
inline void set_large_signed(long long s);
inline std::vector<json_object_ptr> &array();
inline const std::vector<json_object_ptr> &array() const;
@@ -97,9 +102,17 @@ struct json_object {
};
struct json_number : json_object {
double number_value = 0;
unsigned long long large_unsigned_value = 0;
long long large_signed_value = 0;
enum repr_t { REPR_DOUBLE,
REPR_LARGE_UNSIGNED,
REPR_LARGE_SIGNED };
repr_t repr = REPR_DOUBLE;
union value_t {
double d;
unsigned long long u;
long long s;
value_t() : d(0) {}
} value;
json_number() : json_object(JSON_NUMBER) {}
json_number(json_object *p, json_pull *pl) : json_object(JSON_NUMBER, p, pl) {}
@@ -135,29 +148,46 @@ inline const std::string &json_object::string() const {
return static_cast<const json_string *>(this)->string_value;
}
inline double &json_object::number() {
assert(type == JSON_NUMBER);
return static_cast<json_number *>(this)->number_value;
}
inline double json_object::number() const {
assert(type == JSON_NUMBER);
return static_cast<const json_number *>(this)->number_value;
}
inline unsigned long long &json_object::large_unsigned() {
assert(type == JSON_NUMBER);
return static_cast<json_number *>(this)->large_unsigned_value;
auto *n = static_cast<const json_number *>(this);
switch (n->repr) {
case json_number::REPR_LARGE_UNSIGNED:
return static_cast<double>(n->value.u);
case json_number::REPR_LARGE_SIGNED:
return static_cast<double>(n->value.s);
case json_number::REPR_DOUBLE:
default:
return n->value.d;
}
}
inline unsigned long long json_object::large_unsigned() const {
assert(type == JSON_NUMBER);
return static_cast<const json_number *>(this)->large_unsigned_value;
}
inline long long &json_object::large_signed() {
assert(type == JSON_NUMBER);
return static_cast<json_number *>(this)->large_signed_value;
auto *n = static_cast<const json_number *>(this);
return n->repr == json_number::REPR_LARGE_UNSIGNED ? n->value.u : 0;
}
inline long long json_object::large_signed() const {
assert(type == JSON_NUMBER);
return static_cast<const json_number *>(this)->large_signed_value;
auto *n = static_cast<const json_number *>(this);
return n->repr == json_number::REPR_LARGE_SIGNED ? n->value.s : 0;
}
inline void json_object::set_number(double d) {
assert(type == JSON_NUMBER);
auto *n = static_cast<json_number *>(this);
n->repr = json_number::REPR_DOUBLE;
n->value.d = d;
}
inline void json_object::set_large_unsigned(unsigned long long u) {
assert(type == JSON_NUMBER);
auto *n = static_cast<json_number *>(this);
n->repr = json_number::REPR_LARGE_UNSIGNED;
n->value.u = u;
}
inline void json_object::set_large_signed(long long s) {
assert(type == JSON_NUMBER);
auto *n = static_cast<json_number *>(this);
n->repr = json_number::REPR_LARGE_SIGNED;
n->value.s = s;
}
inline std::vector<json_object_ptr> &json_object::array() {