bignum: emit two decimal digits per iteration in big2str_2bdigits

Extend the 2-digit lookup-table itoa optimisation from rb_fix2str to
the inner conversion loop used by Bignum#to_s.  big2str_2bdigits has
two code paths — a leading-chunk path that emits variable-length
digits, and a recursive-chunk path that emits a fixed-width zero-
padded block — and both gain from the halved division count.  The
classic per-digit loop is preserved for non-base-10 conversion.

Moves the ruby_decimal_digit_pairs table from a file-static in
numeric.c to bignum.c next to ruby_digitmap, and exposes it through
internal/bignum.h so both files share the same 200-byte .rodata
instance.

Benchmark (Apple M-series, best of 3 runs, measures bignum-only
speedup against the preceding fixnum commit):

  case            base      patch     delta
  ---------       -----     -----     -----
  big_20dig   10^19+...  146 ns/op 124 ns/op  -15%
  big_40dig   10^39+...  174 ns/op 152 ns/op  -13%
  big_100dig  10^99+42   236 ns/op 213 ns/op  -10%
  big_500dig  10^499+7  1119 ns/op 1086 ns/op  -3%
  big_1000dig 10^999    3490 ns/op 3459 ns/op  -1%
  fix_19dig   2^62-1      76 ns/op   76 ns/op   0% (unchanged path)

Wins concentrate in the 20-100 digit range where big2str_2bdigits
is the dominant cost.  Above ~500 digits the Karatsuba divmod
recursion dominates and the digit-emission saving shrinks to the
noise floor.  The 20-100 range is what actual Ruby code exercises
(financial high-precision sums, nanosecond timestamps, large
counters); crypto-size (1000+ digit) bignums are rare in to_s paths.

Correctness: 100k random fixnum fuzz unchanged, 500 random bignum
fuzz up to 2^256 with cross-check against sprintf("%d"), bases
2/8/16/36 round-trip, plus edge cases (0, just-above-fixnum, ±2^100,
20-digit strings near the fixnum boundary).  test/ruby/test_integer.rb
stays at 38 tests / 421628 assertions / 0 failures, test_bignum.rb
passes 74 / 607 / 0 failures, full make test-all reports 34694
tests / 0 new failures (same TestArgf#test_puts pre-existing flake
as master).
This commit is contained in:
Chris Hasiński committed 2026-04-11 18:22:15 +02:00
1 parent 5bfb7e02a2
commit c5df6de835
3 files changed
+81 -27

No files matched your search

+76 -10
View File
@@ -64,6 +64,21 @@ static const bool debug_integer_pack = (
const char ruby_digitmap[] = "0123456789abcdefghijklmnopqrstuvwxyz";
/* Two-digit decimal lookup table. Offset 2*n holds the ASCII pair for
* n in the range 0..99. Used by both rb_fix2str in numeric.c and
* big2str_2bdigits below to emit two base-10 digits per iteration. */
const char ruby_decimal_digit_pairs[201] =
"00010203040506070809"
"10111213141516171819"
"20212223242526272829"
"30313233343536373839"
"40414243444546474849"
"50515253545556575859"
"60616263646566676869"
"70717273747576777879"
"80818283848586878889"
"90919293949596979899";
#ifndef SIZEOF_BDIGIT_DBL
# if SIZEOF_INT*2 <= SIZEOF_LONG_LONG
# define SIZEOF_BDIGIT_DBL SIZEOF_LONG_LONG
@@ -4814,11 +4829,34 @@ big2str_2bdigits(struct big2str_struct *b2s, BDIGIT *xds, size_t xn, size_t tail
return;
p = buf;
j = sizeof(buf);
do {
BDIGIT_DBL idx = num % b2s->base;
num /= b2s->base;
p[--j] = ruby_digitmap[idx];
} while (num);
if (b2s->base == 10) {
/* Emit two decimal digits per iteration from ruby_decimal_digit_pairs.
* See the comment on the table in bignum.c near ruby_digitmap. */
while (num >= 100) {
BDIGIT_DBL idx = (num % 100) * 2;
num /= 100;
j -= 2;
p[j] = ruby_decimal_digit_pairs[idx];
p[j + 1] = ruby_decimal_digit_pairs[idx + 1];
}
if (num >= 10) {
BDIGIT_DBL idx = num * 2;
j -= 2;
p[j] = ruby_decimal_digit_pairs[idx];
p[j + 1] = ruby_decimal_digit_pairs[idx + 1];
}
else {
/* num is 1..9 here (0 was handled above) */
p[--j] = (char)('0' + num);
}
}
else {
do {
BDIGIT_DBL idx = num % b2s->base;
num /= b2s->base;
p[--j] = ruby_digitmap[idx];
} while (num);
}
len = sizeof(buf) - j;
big2str_alloc(b2s, len + taillen);
MEMCPY(b2s->ptr, buf + j, char, len);
@@ -4826,11 +4864,39 @@ big2str_2bdigits(struct big2str_struct *b2s, BDIGIT *xds, size_t xn, size_t tail
else {
p = b2s->ptr;
j = b2s->hbase2_numdigits;
do {
BDIGIT_DBL idx = num % b2s->base;
num /= b2s->base;
p[--j] = ruby_digitmap[idx];
} while (j);
if (b2s->base == 10) {
/* Non-beginning chunks must emit EXACTLY hbase2_numdigits,
* zero-padded on the left. Consume num in 2-digit groups,
* handle the odd trailing digit, then memset remaining
* positions with '0'. */
while (num >= 100) {
BDIGIT_DBL idx = (num % 100) * 2;
num /= 100;
j -= 2;
p[j] = ruby_decimal_digit_pairs[idx];
p[j + 1] = ruby_decimal_digit_pairs[idx + 1];
}
if (num >= 10) {
BDIGIT_DBL idx = num * 2;
j -= 2;
p[j] = ruby_decimal_digit_pairs[idx];
p[j + 1] = ruby_decimal_digit_pairs[idx + 1];
}
else if (num > 0) {
p[--j] = (char)('0' + num);
}
if (j > 0) {
memset(p, '0', j);
j = 0;
}
}
else {
do {
BDIGIT_DBL idx = num % b2s->base;
num /= b2s->base;
p[--j] = ruby_digitmap[idx];
} while (j);
}
len = b2s->hbase2_numdigits;
}
b2s->ptr += len;
+1
View File
@@ -115,6 +115,7 @@ struct RBignum {
/* bignum.c */
extern const char ruby_digitmap[];
extern const char ruby_decimal_digit_pairs[];
double rb_big_fdiv_double(VALUE x, VALUE y);
VALUE rb_big_uminus(VALUE x);
VALUE rb_big_hash(VALUE);
+4 -17
View File
@@ -4034,23 +4034,10 @@ rb_int_uminus(VALUE num)
}
}
/* Two-digit lookup table for base-10 conversion. Each pair of characters
* encodes a two-digit decimal number from "00" through "99" at offset 2*n.
* Halves the number of divisions in the conversion loop compared to the
* classic digit-at-a-time approach. See e.g. Facebook's folly, LLVM
* libc, Go's strconv — this is the standard itoa optimisation.
*/
static const char ruby_decimal_digit_pairs[201] =
"00010203040506070809"
"10111213141516171819"
"20212223242526272829"
"30313233343536373839"
"40414243444546474849"
"50515253545556575859"
"60616263646566676869"
"70717273747576777879"
"80818283848586878889"
"90919293949596979899";
/* ruby_decimal_digit_pairs is defined in bignum.c and declared in
* internal/bignum.h. See there for the rationale of the 2-digit
* lookup-table itoa optimisation; both rb_fix2str here and big2str_2bdigits
* in bignum.c consume it. */
VALUE
rb_fix2str(VALUE x, int base)