Feature #21650
closedPerformance regression: Rational#floor(ndigits) extremely slow for huge ndigits in Ruby 3.4 (ok in 3.2)
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 ышьшдфкдн фы
Updated by koilanetroc (Oleg Tolmashov) about 14 hours ago
- 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
- 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
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:
- What is your use case for code like
n.floor(2**31)? - 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
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
Integerwould returnFloat::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, when10**nbecame too large, the calculation would effectively "give up" and just returnmincorrectly.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 integern. However, as you can see, Ruby 3.2 returns a non-zero value (evaluates tofalse) for the last line.Running the same code in Ruby 3.4 correctly outputs
truefor 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:
- What is your use case for code like
n.floor(2**31)?- Given that the calculation result was incorrect in Ruby 3.3 and earlier, were you not encountering issues with that?
- 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)
- 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
- 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
Sure, thanks for clarification and fast reply.