Skip to content

Conversation

@mensfeld
Copy link
Contributor

@mensfeld mensfeld commented Dec 18, 2025

This PR integrates the Ryu algorithm for float-to-string conversion, replacing Ruby's current dtoa implementation (David M. Gay's algorithm from 1991) with a modern, faster approach.

Key improvements:

  • Normal floats: ~3.9x faster
  • Random floats: ~3.4x faster
  • Small floats (scientific notation): ~2.9x faster
  • Large integers: ~2.2x faster
  • Overall: ~2.9x faster

Background

The Ryu algorithm (2018, Ulf Adams) uses precomputed 128-bit multiplication tables to achieve O(1) shortest-representation conversion. It has been adopted by:

  • Rust (standard library)
  • Java (JDK internal)
  • Swift
  • V8 (JavaScript engine)

Reference paper: "Ryū: Fast Float-to-String Conversion" (PLDI 2018)

Benchmark Results

Comparison against unmodified Ruby master (same commit) with 2,000,000 iterations:

Test Case Unmodified Optimized Speedup
Simple large integers (i * 1e10) 0.660s 0.299s 2.21x
Complex large (i * 1.23e9) 0.627s 0.275s 2.28x
Normal floats (i * 0.123...) 0.966s 0.249s 3.88x
Small floats (i * 1e-10) 0.809s 0.282s 2.87x
Random floats 1.123s 0.333s 3.37x
TOTAL 4.185s 1.438s 2.91x
#!/usr/bin/env ruby
# Float#to_s benchmark

ITERATIONS = 2_000_000

# Test cases
tests = {
  "Simple large (i * 1e10)" => -> {
    ITERATIONS.times { |i| (i * 1e10).to_s }
  },
  "Complex large (i * 1.23e9)" => -> {
    ITERATIONS.times { |i| (i * 1.23e9).to_s }
  },
  "Normal floats (i * 0.123456789)" => -> {
    ITERATIONS.times { |i| (i * 0.123456789).to_s }
  },
  "Small floats (i * 1e-10)" => -> {
    ITERATIONS.times { |i| (i * 1e-10).to_s }
  },
  "Random floats" => -> {
    srand(42)
    ITERATIONS.times { rand.to_s }
  }
}

total = 0.0
results = {}

tests.each do |name, block|
  GC.start
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  block.call
  elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
  results[name] = elapsed
  total += elapsed
  puts "#{name}: #{elapsed.round(3)}s"
end

puts "-" * 40
puts "TOTAL: #{total.round(3)}s"

Fast Path for Exact Integers

The implementation includes a fast path that detects exact integers in the range [1, 10^15) and converts them directly without invoking the full Ryu algorithm. This ensures no performance regression for common cases like 1e10.to_s while still providing massive speedups for decimal and scientific notation values.

Technical Notes

  • 128-bit arithmetic: Uses __uint128_t on platforms that support it, falls back to portable 64-bit multiplication otherwise
  • Lookup tables: ~34KB of precomputed powers of 5 (smaller table option available with -DRYU_OPTIMIZE_SIZE)
  • Fast path: Direct integer conversion for exact integers < 10^15 (avoids Ryu algorithm overhead)
  • Memory allocation: ruby_ryu_dtoa() returns a malloc'd string (same as ruby_dtoa()), caller frees
  • Thread safety: No global state, fully reentrant
  • License: Ryu is dual-licensed Apache 2.0 / Boost 1.0, compatible with Ruby's BSD-2-Clause

Who Benefits

Any Ruby code that converts floats to strings:

  • JSON serialization (JSON.generate, to_json)
  • Logging and debugging (puts, p, inspect)
  • String interpolation ("Value: #{float}")
  • Template rendering (ERB, etc.)
  • Data export (CSV, XML, etc.)

Note

I am not familiar with the contribution patterns to Ruby core. Please let me know if any changes are needed to align with the project's coding standards or if additional tests are required.

Note 2

I did not remove dtoa.c completely because its used in few other places. If this PR is something that could potentially be accepted I can do sprintf and marshal next.

@mensfeld
Copy link
Contributor Author

I see some of the CI fails with maybe valid reasons but I am not fluent with the Ruby core checks so if that is the case I may need some guidance 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants