From 1abba0a655cb4a4c8e6260deb1d82077b4c0b4f2 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sun, 29 Mar 2026 16:50:13 +0100 Subject: [PATCH] Make BigDecimal object embedded Fix: https://github.com/ruby/bigdecimal/issues/293 BigDecimal is a very good fit for embedded objects, as most of them are small, and they're all immutable, so they don't need any resizing. In most case it results in smaller objects, but also reduces pointer chasing as well as make these objects faster to free for the GC. master: ```ruby >> ObjectSpace.memsize_of(BigDecimal("422343434234234234234234234234423")) => 92 >> ObjectSpace.memsize_of(BigDecimal("4223434342342342342342342342344232342423423")) => 96 ``` This branch: ```ruby >> ObjectSpace.memsize_of(BigDecimal("422343434234234234234234234234423")) => 80 >> ObjectSpace.memsize_of(BigDecimal("4223434342342342342342342342344232342423423")) => 160 ``` --- ext/bigdecimal/bigdecimal.c | 279 ++++++++++++++---------------------- ext/bigdecimal/bigdecimal.h | 7 +- ext/bigdecimal/extconf.rb | 4 + 3 files changed, 116 insertions(+), 174 deletions(-) diff --git a/ext/bigdecimal/bigdecimal.c b/ext/bigdecimal/bigdecimal.c index 97c1ddce..3ef71bda 100644 --- a/ext/bigdecimal/bigdecimal.c +++ b/ext/bigdecimal/bigdecimal.c @@ -77,11 +77,6 @@ static struct { uint8_t mode; } rbd_rounding_modes[RBD_NUM_ROUNDING_MODES]; -typedef struct { - VALUE bigdecimal_or_nil; - Real *real_or_null; -} NULLABLE_BDVALUE; - static inline BDVALUE bdvalue_nonnullable(NULLABLE_BDVALUE v) { @@ -157,42 +152,6 @@ rbd_struct_size(size_t const internal_digits) return offsetof(Real, frac) + frac_len * sizeof(DECDIG); } -static inline Real * -rbd_allocate_struct(size_t const internal_digits) -{ - size_t const size = rbd_struct_size(internal_digits); - Real *real = ruby_xcalloc(1, size); - atomic_allocation_count_inc(); - real->MaxPrec = internal_digits; - return real; -} - -static inline Real * -rbd_allocate_struct_decimal_digits(size_t const decimal_digits) -{ - return rbd_allocate_struct(roomof(decimal_digits, BASE_FIG)); -} - -static void -rbd_free_struct(Real *real) -{ - if (real != NULL) { - check_allocation_count_nonzero(); - ruby_xfree(real); - atomic_allocation_count_dec_nounderflow(); - } -} - -MAYBE_UNUSED(static inline Real * rbd_allocate_struct_zero(int sign, size_t const digits)); -#define NewZero rbd_allocate_struct_zero -static inline Real * -rbd_allocate_struct_zero(int sign, size_t const digits) -{ - Real *real = rbd_allocate_struct_decimal_digits(digits); - VpSetZero(real, sign); - return real; -} - /* * ================== Ruby Interface part ========================== */ @@ -207,7 +166,6 @@ static void VpCheckException(Real *p, bool always); static VALUE CheckGetValue(BDVALUE v); static void VpInternalRound(Real *c, size_t ixDigit, DECDIG vPrev, DECDIG v); static int VpLimitRound(Real *c, size_t ixDigit); -static Real *VpCopy(Real *pv, Real const* const x); static int VPrint(FILE *fp,const char *cntl_chr,Real *a); /* @@ -222,49 +180,67 @@ static VALUE BigDecimal_negative_zero(void); static VALUE BigDecimal_addsub_with_coerce(VALUE self, VALUE r, size_t prec, int operation); static VALUE BigDecimal_mult_with_coerce(VALUE self, VALUE r, size_t prec); -static void -BigDecimal_delete(void *pv) -{ - rbd_free_struct(pv); -} +#ifndef HAVE_RB_EXT_RACTOR_SAFE +# undef RUBY_TYPED_FROZEN_SHAREABLE +# define RUBY_TYPED_FROZEN_SHAREABLE 0 +#endif + +#ifdef RUBY_TYPED_EMBEDDABLE +# define HAVE_RUBY_TYPED_EMBEDDABLE 1 +#else +# ifdef HAVE_CONST_RUBY_TYPED_EMBEDDABLE +# define RUBY_TYPED_EMBEDDABLE RUBY_TYPED_EMBEDDABLE +# define HAVE_RUBY_TYPED_EMBEDDABLE 1 +# else +# define RUBY_TYPED_EMBEDDABLE 0 +# endif +#endif static size_t BigDecimal_memsize(const void *ptr) { +#ifdef HAVE_RUBY_TYPED_EMBEDDABLE + return 0; // Entirely embedded +#else const Real *pv = ptr; return (sizeof(*pv) + pv->MaxPrec * sizeof(DECDIG)); -} - -#ifndef HAVE_RB_EXT_RACTOR_SAFE -# undef RUBY_TYPED_FROZEN_SHAREABLE -# define RUBY_TYPED_FROZEN_SHAREABLE 0 #endif +} static const rb_data_type_t BigDecimal_data_type = { - "BigDecimal", - { 0, BigDecimal_delete, BigDecimal_memsize, }, -#ifdef RUBY_TYPED_FREE_IMMEDIATELY - 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_FROZEN_SHAREABLE | RUBY_TYPED_WB_PROTECTED -#endif + .wrap_struct_name = "BigDecimal", + .function = { + .dmark = 0, + .dfree = RUBY_DEFAULT_FREE, + .dsize = BigDecimal_memsize, + }, + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_FROZEN_SHAREABLE | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE, }; -// TypedData_Wrap_Struct may fail if there is no memory, or GC.add_stress_to_class(BigDecimal) is set. -// We need to first allocate empty struct, allocate Real struct, and then set the data pointer. -typedef struct { VALUE _obj; } NULL_WRAPPED_VALUE; -static NULL_WRAPPED_VALUE -BigDecimal_alloc_empty_struct(VALUE klass) +static VALUE +BigDecimal_allocate(size_t const internal_digits) { - return (NULL_WRAPPED_VALUE) { TypedData_Wrap_Struct(klass, &BigDecimal_data_type, NULL) }; + const size_t size = rbd_struct_size(internal_digits); + VALUE bd = rb_data_typed_object_zalloc(rb_cBigDecimal, size, &BigDecimal_data_type); + Real *vp; + TypedData_Get_Struct(bd, Real, &BigDecimal_data_type, vp); + vp->MaxPrec = internal_digits; + RB_OBJ_FREEZE(bd); + return bd; } static VALUE -BigDecimal_wrap_struct(NULL_WRAPPED_VALUE v, Real *real) +BigDecimal_allocate_decimal_digits(size_t const decimal_digits) { - VALUE obj = v._obj; - assert(RTYPEDDATA_DATA(obj) == NULL); - RTYPEDDATA_DATA(obj) = real; - RB_OBJ_FREEZE(obj); - return obj; + return BigDecimal_allocate(roomof(decimal_digits, BASE_FIG)); +} + +static Real * +VpPtr(VALUE obj) +{ + Real *vp; + TypedData_Get_Struct(obj, Real, &BigDecimal_data_type, vp); + return vp; } MAYBE_UNUSED(static inline BDVALUE rbd_allocate_struct_zero_wrap(int sign, size_t const digits)); @@ -272,9 +248,10 @@ MAYBE_UNUSED(static inline BDVALUE rbd_allocate_struct_zero_wrap(int sign, size_ static BDVALUE rbd_allocate_struct_zero_wrap(int sign, size_t const digits) { - NULL_WRAPPED_VALUE null_wrapped = BigDecimal_alloc_empty_struct(rb_cBigDecimal); - Real *real = rbd_allocate_struct_zero(sign, digits); - return (BDVALUE) { BigDecimal_wrap_struct(null_wrapped, real), real }; + VALUE obj = BigDecimal_allocate_decimal_digits(digits); + Real *real = VpPtr(obj); + VpSetZero(real, sign); + return (BDVALUE) { obj, real }; } static inline int @@ -336,8 +313,7 @@ GetBDValueWithPrecInternal(VALUE v, size_t prec, int must) goto SomeOneMayDoIt; } - Real *vp; - TypedData_Get_Struct(v, Real, &BigDecimal_data_type, vp); + Real *vp = VpPtr(v); return (NULLABLE_BDVALUE) { v, vp }; SomeOneMayDoIt: @@ -1010,26 +986,18 @@ check_int_precision(VALUE v) static NULLABLE_BDVALUE CreateFromString(const char *str, VALUE klass, bool strict_p, bool raise_exception) { - NULL_WRAPPED_VALUE null_wrapped = BigDecimal_alloc_empty_struct(klass); - Real *pv = VpAlloc(str, strict_p, raise_exception); - if (!pv) return (NULLABLE_BDVALUE) { Qnil, NULL }; - return (NULLABLE_BDVALUE) { BigDecimal_wrap_struct(null_wrapped, pv), pv }; + return VpAlloc(str, strict_p, raise_exception); } -static Real * -VpCopy(Real *pv, Real const* const x) +void +VpMemCopy(Real *pv, Real const* const x) { - assert(x != NULL); - - pv = (Real *)ruby_xrealloc(pv, rbd_struct_size(x->MaxPrec)); pv->MaxPrec = x->MaxPrec; pv->Prec = x->Prec; pv->exponent = x->exponent; pv->sign = x->sign; pv->flag = x->flag; MEMCPY(pv->frac, x->frac, DECDIG, pv->MaxPrec); - - return pv; } /* Returns True if the value is Not a Number. */ @@ -1219,7 +1187,7 @@ GetCoercePrec(Real *a, size_t prec) static VALUE BigDecimal_coerce(VALUE self, VALUE other) { - Real* pv = DATA_PTR(self); + Real* pv = VpPtr(self); BDVALUE b = GetBDValueWithPrecMust(other, GetCoercePrec(pv, 0)); return rb_assoc_new(CheckGetValue(b), self); } @@ -1687,7 +1655,7 @@ BigDecimal_DoDivmod(VALUE self, VALUE r, NULLABLE_BDVALUE *div, NULLABLE_BDVALUE if (VpIsNaN(a.real) || VpIsNaN(b.real) || (VpIsInf(a.real) && VpIsInf(b.real))) { VALUE nan = BigDecimal_nan(); - *div = *mod = (NULLABLE_BDVALUE) { nan, DATA_PTR(nan) }; + *div = *mod = (NULLABLE_BDVALUE) { nan, VpPtr(nan) }; goto Done; } if (VpIsZero(b.real)) { @@ -1696,19 +1664,19 @@ BigDecimal_DoDivmod(VALUE self, VALUE r, NULLABLE_BDVALUE *div, NULLABLE_BDVALUE if (VpIsInf(a.real)) { if (VpGetSign(a.real) == VpGetSign(b.real)) { VALUE inf = BigDecimal_positive_infinity(); - *div = (NULLABLE_BDVALUE) { inf, DATA_PTR(inf) }; + *div = (NULLABLE_BDVALUE) { inf, VpPtr(inf) }; } else { VALUE inf = BigDecimal_negative_infinity(); - *div = (NULLABLE_BDVALUE) { inf, DATA_PTR(inf) }; + *div = (NULLABLE_BDVALUE) { inf, VpPtr(inf) }; } VALUE nan = BigDecimal_nan(); - *mod = (NULLABLE_BDVALUE) { nan, DATA_PTR(nan) }; + *mod = (NULLABLE_BDVALUE) { nan, VpPtr(nan) }; goto Done; } if (VpIsZero(a.real)) { VALUE zero = BigDecimal_positive_zero(); - *div = (NULLABLE_BDVALUE) { zero, DATA_PTR(zero) }; + *div = (NULLABLE_BDVALUE) { zero, VpPtr(zero) }; *mod = bdvalue_nullable(a); goto Done; } @@ -1722,7 +1690,7 @@ BigDecimal_DoDivmod(VALUE self, VALUE r, NULLABLE_BDVALUE *div, NULLABLE_BDVALUE *mod = bdvalue_nullable(b); } else { VALUE zero = BigDecimal_positive_zero(); - *div = (NULLABLE_BDVALUE) { zero, DATA_PTR(zero) }; + *div = (NULLABLE_BDVALUE) { zero, VpPtr(zero) }; *mod = bdvalue_nullable(a); } goto Done; @@ -2566,9 +2534,7 @@ check_exception(VALUE bd) { assert(is_kind_of_BigDecimal(bd)); - Real *vp; - TypedData_Get_Struct(bd, Real, &BigDecimal_data_type, vp); - VpCheckException(vp, false); + VpCheckException(VpPtr(bd), false); return bd; } @@ -2576,17 +2542,19 @@ check_exception(VALUE bd) static VALUE rb_uint64_convert_to_BigDecimal(uint64_t uval) { - NULL_WRAPPED_VALUE null_wrapped = BigDecimal_alloc_empty_struct(rb_cBigDecimal); + VALUE bd; Real *vp; if (uval == 0) { - vp = rbd_allocate_struct(1); + bd = BigDecimal_allocate(1); + vp = VpPtr(bd); vp->Prec = 1; vp->exponent = 1; VpSetZero(vp, 1); vp->frac[0] = 0; } else if (uval < BASE) { - vp = rbd_allocate_struct(1); + bd = BigDecimal_allocate(1); + vp = VpPtr(bd); vp->Prec = 1; vp->exponent = 1; VpSetSign(vp, 1); @@ -2611,14 +2579,15 @@ rb_uint64_convert_to_BigDecimal(uint64_t uval) } const size_t exp = len + ntz; - vp = rbd_allocate_struct(len); + bd = BigDecimal_allocate(len); + vp = VpPtr(bd); vp->Prec = len; vp->exponent = exp; VpSetSign(vp, 1); MEMCPY(vp->frac, buf + BIGDECIMAL_INT64_MAX_LENGTH - len, DECDIG, len); } - return BigDecimal_wrap_struct(null_wrapped, vp); + return bd; } static VALUE @@ -2627,8 +2596,7 @@ rb_int64_convert_to_BigDecimal(int64_t ival) const uint64_t uval = (ival < 0) ? (((uint64_t)-(ival+1))+1) : (uint64_t)ival; VALUE bd = rb_uint64_convert_to_BigDecimal(uval); if (ival < 0) { - Real *vp; - TypedData_Get_Struct(bd, Real, &BigDecimal_data_type, vp); + Real *vp = VpPtr(bd); VpSetSign(vp, -1); } return bd; @@ -2835,8 +2803,7 @@ rb_float_convert_to_BigDecimal(VALUE val, size_t digs, int raise_exception) } VALUE bd = rb_inum_convert_to_BigDecimal(inum); - Real *vp; - TypedData_Get_Struct(bd, Real, &BigDecimal_data_type, vp); + Real *vp = VpPtr(bd); assert(vp->Prec == prec); vp->exponent = exp; @@ -2902,13 +2869,15 @@ rb_convert_to_BigDecimal(VALUE val, size_t digs, int raise_exception) if (digs == SIZE_MAX) return check_exception(val); - NULL_WRAPPED_VALUE null_wrapped = BigDecimal_alloc_empty_struct(rb_cBigDecimal); - Real *vp; - TypedData_Get_Struct(val, Real, &BigDecimal_data_type, vp); - vp = VpCopy(NULL, vp); + Real *vp = VpPtr(val); + + VALUE copy = BigDecimal_allocate(vp->MaxPrec); + Real *vp_copy = VpPtr(copy); + + VpMemCopy(vp_copy, vp); + RB_GC_GUARD(val); - VALUE copy = BigDecimal_wrap_struct(null_wrapped, vp); /* TODO: rounding */ return check_exception(copy); } @@ -3707,7 +3676,7 @@ Init_bigdecimal(void) static int gfDebug = 1; /* Debug switch */ #endif /* BIGDECIMAL_DEBUG */ -static Real *VpConstOne; /* constant 1.0 */ +static VALUE VpConstOne; /* constant 1.0 */ enum op_sw { OP_SW_ADD = 1, /* + */ @@ -4108,8 +4077,9 @@ VpInit(DECDIG BaseVal) VpGetDoubleNegZero(); /* Const 1.0 */ - VpConstOne = NewZero(1, 1); - VpSetOne(VpConstOne); + rb_global_variable(&VpConstOne); + VpConstOne = NewZeroWrap(1, 1).bigdecimal; + VpSetOne(VpPtr(VpConstOne)); #ifdef BIGDECIMAL_DEBUG gnAlloc = 0; @@ -4121,7 +4091,7 @@ VpInit(DECDIG BaseVal) VP_EXPORT Real * VpOne(void) { - return VpConstOne; + return VpPtr(VpConstOne); } /* If exponent overflows,then raise exception or returns 0 */ @@ -4152,7 +4122,7 @@ AddExponent(Real *a, SIGNED_VALUE n) return VpException(VP_EXCEPTION_OVERFLOW, "Exponent overflow", 0); } -Real * +NULLABLE_BDVALUE bigdecimal_parse_special_string(const char *str) { static const struct { @@ -4177,66 +4147,27 @@ bigdecimal_parse_special_string(const char *str) p = str + table[i].len; while (*p && ISSPACE(*p)) ++p; if (*p == '\0') { - Real *vp = rbd_allocate_struct(1); + VALUE obj = BigDecimal_allocate(1); + Real *vp = VpPtr(obj); switch (table[i].sign) { default: - UNREACHABLE; break; + UNREACHABLE; + return (NULLABLE_BDVALUE) { Qnil, NULL }; case VP_SIGN_POSITIVE_INFINITE: VpSetPosInf(vp); - return vp; + break; case VP_SIGN_NEGATIVE_INFINITE: VpSetNegInf(vp); - return vp; + break; case VP_SIGN_NaN: VpSetNaN(vp); - return vp; + break; } + return (NULLABLE_BDVALUE) { obj, vp }; } } - return NULL; -} - -struct VpCtoV_args { - Real *a; - const char *int_chr; - size_t ni; - const char *frac; - size_t nf; - const char *exp_chr; - size_t ne; -}; - -static VALUE -call_VpCtoV(VALUE arg) -{ - struct VpCtoV_args *x = (struct VpCtoV_args *)arg; - return (VALUE)VpCtoV(x->a, x->int_chr, x->ni, x->frac, x->nf, x->exp_chr, x->ne); -} - -static int -protected_VpCtoV(Real *a, const char *int_chr, size_t ni, const char *frac, size_t nf, const char *exp_chr, size_t ne, int free_on_error) -{ - struct VpCtoV_args args; - int state = 0; - - args.a = a; - args.int_chr = int_chr; - args.ni = ni; - args.frac = frac; - args.nf = nf; - args.exp_chr = exp_chr; - args.ne = ne; - - VALUE result = rb_protect(call_VpCtoV, (VALUE)&args, &state); - if (state) { - if (free_on_error) { - rbd_free_struct(a); - } - rb_jump_tag(state); - } - - return (int)result; + return (NULLABLE_BDVALUE) { Qnil, NULL }; } /* @@ -4245,25 +4176,25 @@ protected_VpCtoV(Real *a, const char *int_chr, size_t ni, const char *frac, size * szVal ... The value assigned(char). * * [Returns] - * Pointer to the newly allocated variable, or - * NULL be returned if memory allocation is failed,or any error. + * NULLABLE_BDVALUE to the newly allocated variable. + * Null is returned if memory allocation failed, or any error occured. */ -VP_EXPORT Real * +VP_EXPORT NULLABLE_BDVALUE VpAlloc(const char *szVal, int strict_p, int exc) { const char *orig_szVal = szVal; size_t i, j, ni, ipf, nf, ipe, ne, exp_seen, nalloc; char v, *psz; int sign=1; - Real *vp = NULL; VALUE buf; /* Skipping leading spaces */ while (ISSPACE(*szVal)) szVal++; /* Check on Inf & NaN */ - if ((vp = bigdecimal_parse_special_string(szVal)) != NULL) { - return vp; + NULLABLE_BDVALUE special_bd = bigdecimal_parse_special_string(szVal); + if (special_bd.real_or_null != NULL) { + return special_bd; } /* Skip leading `#`. @@ -4417,10 +4348,11 @@ VpAlloc(const char *szVal, int strict_p, int exc) VALUE str; invalid_value: if (!strict_p) { - return NewZero(1, 1); + BDVALUE res = rbd_allocate_struct_zero_wrap(1, 1); + return (NULLABLE_BDVALUE) { res.bigdecimal, res.real }; } if (!exc) { - return NULL; + return (NULLABLE_BDVALUE) { Qnil, NULL }; } str = rb_str_new2(orig_szVal); rb_raise(rb_eArgError, "invalid value for BigDecimal(): \"%"PRIsVALUE"\"", str); @@ -4428,11 +4360,12 @@ VpAlloc(const char *szVal, int strict_p, int exc) nalloc = (ni + nf + BASE_FIG - 1) / BASE_FIG + 1; /* set effective allocation */ /* units for szVal[] */ - vp = rbd_allocate_struct(nalloc); + VALUE obj = BigDecimal_allocate(nalloc); + Real *vp = VpPtr(obj); VpSetZero(vp, sign); - protected_VpCtoV(vp, psz, ni, psz + ipf, nf, psz + ipe, ne, true); + VpCtoV(vp, psz, ni, psz + ipf, nf, psz + ipe, ne); rb_str_resize(buf, 0); - return vp; + return (NULLABLE_BDVALUE) { obj, vp }; } /* diff --git a/ext/bigdecimal/bigdecimal.h b/ext/bigdecimal/bigdecimal.h index 57ae4bb0..faa66264 100644 --- a/ext/bigdecimal/bigdecimal.h +++ b/ext/bigdecimal/bigdecimal.h @@ -168,6 +168,11 @@ typedef struct { Real *real; } BDVALUE; +typedef struct { + VALUE bigdecimal_or_nil; + Real *real_or_null; +} NULLABLE_BDVALUE; + /* * ------------------ * EXPORTables. @@ -194,7 +199,7 @@ VP_EXPORT unsigned short VpSetRoundMode(unsigned short n); VP_EXPORT int VpException(unsigned short f,const char *str,int always); VP_EXPORT size_t VpNumOfChars(Real *vp,const char *pszFmt); VP_EXPORT size_t VpInit(DECDIG BaseVal); -VP_EXPORT Real *VpAlloc(const char *szVal, int strict_p, int exc); +VP_EXPORT NULLABLE_BDVALUE VpAlloc(const char *szVal, int strict_p, int exc); VP_EXPORT size_t VpAsgn(Real *c, Real *a, int isw); VP_EXPORT size_t VpAddSub(Real *c,Real *a,Real *b,int operation); VP_EXPORT size_t VpMult(Real *c,Real *a,Real *b); diff --git a/ext/bigdecimal/extconf.rb b/ext/bigdecimal/extconf.rb index e1897e6c..0b4baca2 100644 --- a/ext/bigdecimal/extconf.rb +++ b/ext/bigdecimal/extconf.rb @@ -46,6 +46,10 @@ def have_builtin_func(name, check_expr, opt = "", &b) have_func("rb_category_warn", "ruby.h") have_const("RB_WARN_CATEGORY_DEPRECATED", "ruby.h") +if RUBY_ENGINE == "ruby" + have_const("RUBY_TYPED_EMBEDDABLE", "ruby.h") # RUBY_VERSION >= 3.3 +end + if File.file?(File.expand_path('../lib/bigdecimal.rb', __FILE__)) bigdecimal_rb = "$(srcdir)/lib/bigdecimal.rb" else