Skip to content

Commit b66add1

Browse files
authored
Optimize BigDecimal#to_s (#519)
Most of the time was spent in two calls to `snprintf`, by a simpler integer to ASCII function, it can be made several time faster. The code is largely adapted from an earlier version of ruby/json. ruby/json now use a much more optimized algorithm, but there are licensing consideration so not sure it's worth optimizing that much. Before: ``` ruby 4.0.2 (2026-03-17 revision d3da9fec82) +YJIT +PRISM [arm64-darwin25] Warming up -------------------------------------- 22.99 408.215k i/100ms large 230.802k i/100ms Calculating ------------------------------------- 22.99 4.214M (± 2.2%) i/s (237.32 ns/i) - 21.227M in 5.040152s large 2.384M (± 2.6%) i/s (419.45 ns/i) - 12.002M in 5.037698s ``` After: ``` ruby 4.0.2 (2026-03-17 revision d3da9fec82) +YJIT +PRISM [arm64-darwin25] Warming up -------------------------------------- 22.99 1.026M i/100ms large 846.057k i/100ms Calculating ------------------------------------- 22.99 10.882M (± 0.8%) i/s (91.89 ns/i) - 55.426M in 5.093603s large 9.094M (± 1.0%) i/s (109.97 ns/i) - 45.687M in 5.024549s ``` ```ruby require "bundler/inline" gemfile do source 'https://rubygems.org' gem "benchmark-ips" gem "bigdecimal", path: "/Users/byroot/src/github.com/byroot/bigdecimal" end small = BigDecimal("29.99") large = BigDecimal("32423094234234.23423432") Benchmark.ips do |x| x.report("22.99") { small.to_s } x.report("large") { large.to_s } end ```
1 parent 219cb2e commit b66add1

File tree

1 file changed

+31
-3
lines changed

1 file changed

+31
-3
lines changed

ext/bigdecimal/bigdecimal.c

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5449,10 +5449,22 @@ VpToSpecialString(Real *a, char *buf, size_t buflen, int fPlus)
54495449
return 0;
54505450
}
54515451

5452+
#define ULLTOA_BUFFER_SIZE 20
5453+
static size_t Vp_ulltoa(unsigned long long number, char *buf)
5454+
{
5455+
static const char digits[] = "0123456789";
5456+
char* tmp = buf;
5457+
5458+
do *tmp-- = digits[number % 10]; while (number /= 10);
5459+
return buf - tmp;
5460+
}
5461+
54525462
VP_EXPORT void
54535463
VpToString(Real *a, char *buf, size_t buflen, size_t fFmt, int fPlus)
54545464
/* fPlus = 0: default, 1: set ' ' before digits, 2: set '+' before digits. */
54555465
{
5466+
char ulltoa_buf[ULLTOA_BUFFER_SIZE];
5467+
char *ulltoa_buf_end = ulltoa_buf + ULLTOA_BUFFER_SIZE;
54565468
size_t i, n, ZeroSup;
54575469
DECDIG shift, m, e, nn;
54585470
char *p = buf;
@@ -5492,10 +5504,11 @@ VpToString(Real *a, char *buf, size_t buflen, size_t fFmt, int fPlus)
54925504
while (m) {
54935505
nn = e / m;
54945506
if (!ZeroSup || nn) {
5495-
/* The reading zero(s) */
5496-
size_t n = (size_t)snprintf(p, plen, "%lu", (unsigned long)nn);
5507+
size_t n = Vp_ulltoa(nn, ulltoa_buf_end - 1);
54975508
if (n > plen) goto overflow;
5509+
MEMCPY(p, ulltoa_buf_end - n, char, n);
54985510
ADVANCE(n);
5511+
54995512
/* as 0.00xx will be ignored. */
55005513
ZeroSup = 0; /* Set to print succeeding zeros */
55015514
}
@@ -5514,7 +5527,22 @@ VpToString(Real *a, char *buf, size_t buflen, size_t fFmt, int fPlus)
55145527
*(--p) = '\0';
55155528
++plen;
55165529
}
5517-
snprintf(p, plen, "e%"PRIdSIZE, ex);
5530+
*p = 'e';
5531+
ADVANCE(1);
5532+
5533+
if (ex < 0) {
5534+
*p = '-';
5535+
ADVANCE(1);
5536+
ex = -ex;
5537+
}
5538+
5539+
size_t ex_n = Vp_ulltoa(ex, ulltoa_buf_end - 1);
5540+
if (ex_n > plen) goto overflow;
5541+
MEMCPY(p, ulltoa_buf_end - ex_n, char, ex_n);
5542+
ADVANCE(ex_n);
5543+
*p = '\0';
5544+
ADVANCE(1);
5545+
55185546
if (fFmt) VpFormatSt(buf, fFmt);
55195547

55205548
overflow:

0 commit comments

Comments
 (0)