Project

General

Profile

Actions

Feature #21650

closed

Performance regression: Rational#floor(ndigits) extremely slow for huge ndigits in Ruby 3.4 (ok in 3.2)

Feature #21650: Performance regression: Rational#floor(ndigits) extremely slow for huge ndigits in Ruby 3.4 (ok in 3.2)

Added by koilanetroc (Oleg Tolmashov) about 14 hours ago. Updated about 13 hours ago.

Status:
Feedback
Assignee:
-
Target version:
-
[ruby-core:123558]

Description

Summary

Rational#floor(ndigits) with a very large positive ndigits takes tens of seconds in Ruby 3.4, while it returns essentially instantly in Ruby 3.2. Reproducible on macOS and Linux. Looks like a missing fast‑path for rationals whose decimal expansion terminates.

Steps to reproduce

require "benchmark"

puts RUBY_DESCRIPTION
t = Benchmark.realtime { (2 ** -3).floor(2 ** 31) }
puts "elapsed: #{t.round(3)}s"

Also reproduces with the explicit rational form:

Benchmark.realtime { Rational(1, 8).floor(2 ** 31) }

Results on my machine

Ruby 3.2.8:

ruby 3.2.8 (2025-03-26 revision 13f495dc2c) [arm64-darwin24]
slow_math.rb:4: warning: in a**b, b may be too big
elapsed: 0.0s

Ruby 3.4.7:

ruby 3.4.7 (2025-10-08 revision 7a5688e2a2) +PRISM [arm64-darwin24]
elapsed: 39.214s

Actual behavior

On Ruby 3.4.x this call takes ~tens of seconds (e.g., ~40s on my machine), consuming CPU. Same on macOS and Linux.

Expected behavior

The method should return quickly ышьшдфкдн фы


Related issues 1 (0 open1 closed)

Related to Ruby - Feature #20811: `warning: in a**b, b may be too big` is really helpful?ClosedActions

Updated by koilanetroc (Oleg Tolmashov) about 14 hours ago Actions #1

  • Subject changed from Performance regression: `Rational#floor(ndigits)` extremely slow for huge ndigits in Ruby 3.4 (ok in 3.2) to Performance regression: Rational#floor(ndigits) extremely slow for huge ndigits in Ruby 3.4 (ok in 3.2)
  • Description updated (diff)

Updated by mame (Yusuke Endoh) about 14 hours ago Actions #2

  • Related to Feature #20811: `warning: in a**b, b may be too big` is really helpful? added

Updated by mame (Yusuke Endoh) about 14 hours ago · Edited Actions #3 [ruby-core:123559]

Thank you for the report.

As the warning indicates, prior to Ruby 3.4 (up to 3.3), attempting to generate a huge Integer would return Float::INFINITY. Ruby 3.4 removed this inaccurate truncation, which is why it now takes longer. (#20811)

The calculation for m.floor(n) is computed as (m * 10**n).floor / (10**n). In Ruby 3.3 and earlier, when 10**n became too large, the calculation would effectively "give up" and just return m incorrectly.

This wasn't just fast; it was producing an incorrect result. Please look at the following example:

# returns Rational(1, 10**(2**n))
def calc(n)
  d = 10
  n.times { d *= d }
  Rational(1, d)
end

p calc(1) #=> (1/100)
p calc(2) #=> (1/10000)
p calc(3) #=> (1/100000000)

p calc(10).floor(2**10 - 1) == 0 #=> true  # correct
p calc(20).floor(2**20 - 1) == 0 #=> true  # correct
p calc(30).floor(2**30 - 1) == 0 #=> false # incorrect in Ruby 3.2

Rational(1, 10**(2**n)).floor(2**n - 1) should return 0 for any integer n. However, as you can see, Ruby 3.2 returns a non-zero value (evaluates to false) for the last line.

Running the same code in Ruby 3.4 correctly outputs true for all cases.

Now, while there might be room to improve the algorithm and speed up Rational#floor, I would like to confirm a few things first:

  1. What is your use case for code like n.floor(2**31)?
  2. Given that the calculation result was incorrect in Ruby 3.3 and earlier, were you not encountering issues with that?

Updated by koilanetroc (Oleg Tolmashov) about 14 hours ago Actions #4 [ruby-core:123561]

mame (Yusuke Endoh) wrote in #note-3:

Thank you for the report.

As the warning indicates, prior to Ruby 3.4 (up to 3.3), attempting to generate a huge Integer would return Float::INFINITY. Ruby 3.4 removed this inaccurate truncation, which is why it now takes longer. (#20811)

The calculation for m.floor(n) is computed as (m * 10**n).floor / (10**n). In Ruby 3.3 and earlier, when 10**n became too large, the calculation would effectively "give up" and just return m incorrectly.

This wasn't just fast; it was producing an incorrect result. Please look at the following example:

# returns Rational(1, 10**(2**n))
def calc(n)
  d = 10
  n.times { d *= d }
  Rational(1, d)
end

p calc(1) #=> (1/100)
p calc(2) #=> (1/10000)
p calc(3) #=> (1/1000000)

p calc(10).floor(2**10 - 1) == 0 #=> true  # correct
p calc(20).floor(2**20 - 1) == 0 #=> true  # correct
p calc(30).floor(2**30 - 1) == 0 #=> false # incorrect in Ruby 3.2

Rational(1, 10**(2**n)).floor(2**n - 1) should return 0 for any integer n. However, as you can see, Ruby 3.2 returns a non-zero value (evaluates to false) for the last line.

Running the same code in Ruby 3.4 correctly outputs true for all cases.

Now, while there might be room to improve the algorithm and speed up Rational#floor, I would like to confirm a few things first:

  1. What is your use case for code like n.floor(2**31)?
  2. Given that the calculation result was incorrect in Ruby 3.3 and earlier, were you not encountering issues with that?
  1. It wasn't involved in any production usage, we have fuzzy tests which generated this and it was in our test suite. Therefore I was surprised by performance degradation after ruby upgrade(3.2 -> 3.4)
  2. No, as we were not validating that results were actually correct, just compared that answer is same

Updated by mame (Yusuke Endoh) about 13 hours ago Actions #5 [ruby-core:123563]

  • Tracker changed from Bug to Feature
  • Status changed from Open to Feedback
  • ruby -v deleted (ruby 3.4.7 (2025-10-08 revision 7a5688e2a2) +PRISM [arm64-darwin24])
  • Backport deleted (3.2: UNKNOWN, 3.3: UNKNOWN, 3.4: UNKNOWN)

Thank you for your reply.

I found a clearer case:

p Rational(1, 3).floor(2**1) #=> (33/100)
p Rational(1, 3).floor(2**2) #=> (3333/10000)
p Rational(1, 3).floor(2**3) #=> (33333333/100000000)
p Rational(1, 3).floor(2**4) #=> (3333333333333333/10000000000000000)

# in Ruby 3.3
p Rational(1, 3).floor(2**30) #=> warning: in a**b, b may be too big
                              #=> (1/3) 

Also, I think you can immediately see that this operation generally cannot be O(1).

An improvement might be possible by creating a specialized "fast path" for cases where the denominator's prime factors are only 2 and 5. I will move this ticket to the Feature tracker. However, if there is no use case for this, we may need to consider if the added complexity is worth it.

Updated by koilanetroc (Oleg Tolmashov) about 13 hours ago Actions #6 [ruby-core:123564]

Sure, thanks for clarification and fast reply.

Actions

Also available in: PDF Atom