From bda9134c849c777b47b6b242bad9f38e5c4776ee Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 6 Apr 2026 14:53:35 +0200 Subject: [PATCH] Optimize BigDecimal#to_s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ``` --- ext/bigdecimal/bigdecimal.c | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/ext/bigdecimal/bigdecimal.c b/ext/bigdecimal/bigdecimal.c index 555abebc..3234a894 100644 --- a/ext/bigdecimal/bigdecimal.c +++ b/ext/bigdecimal/bigdecimal.c @@ -5449,10 +5449,22 @@ VpToSpecialString(Real *a, char *buf, size_t buflen, int fPlus) return 0; } +#define ULLTOA_BUFFER_SIZE 20 +static size_t Vp_ulltoa(unsigned long long number, char *buf) +{ + static const char digits[] = "0123456789"; + char* tmp = buf; + + do *tmp-- = digits[number % 10]; while (number /= 10); + return buf - tmp; +} + VP_EXPORT void VpToString(Real *a, char *buf, size_t buflen, size_t fFmt, int fPlus) /* fPlus = 0: default, 1: set ' ' before digits, 2: set '+' before digits. */ { + char ulltoa_buf[ULLTOA_BUFFER_SIZE]; + char *ulltoa_buf_end = ulltoa_buf + ULLTOA_BUFFER_SIZE; size_t i, n, ZeroSup; DECDIG shift, m, e, nn; char *p = buf; @@ -5492,10 +5504,11 @@ VpToString(Real *a, char *buf, size_t buflen, size_t fFmt, int fPlus) while (m) { nn = e / m; if (!ZeroSup || nn) { - /* The reading zero(s) */ - size_t n = (size_t)snprintf(p, plen, "%lu", (unsigned long)nn); + size_t n = Vp_ulltoa(nn, ulltoa_buf_end - 1); if (n > plen) goto overflow; + MEMCPY(p, ulltoa_buf_end - n, char, n); ADVANCE(n); + /* as 0.00xx will be ignored. */ ZeroSup = 0; /* Set to print succeeding zeros */ } @@ -5514,7 +5527,22 @@ VpToString(Real *a, char *buf, size_t buflen, size_t fFmt, int fPlus) *(--p) = '\0'; ++plen; } - snprintf(p, plen, "e%"PRIdSIZE, ex); + *p = 'e'; + ADVANCE(1); + + if (ex < 0) { + *p = '-'; + ADVANCE(1); + ex = -ex; + } + + size_t ex_n = Vp_ulltoa(ex, ulltoa_buf_end - 1); + if (ex_n > plen) goto overflow; + MEMCPY(p, ulltoa_buf_end - ex_n, char, ex_n); + ADVANCE(ex_n); + *p = '\0'; + ADVANCE(1); + if (fFmt) VpFormatSt(buf, fFmt); overflow: