[ruby-core:123558] [Ruby Bug#21650] Performance regression: `Rational#floor(ndigits)` extremely slow for huge ndigits in Ruby 3.4 (ok in 3.2)
Issue #21650 has been reported by koilanetroc (Oleg Tolmashov). ---------------------------------------- Bug #21650: Performance regression: `Rational#floor(ndigits)` extremely slow for huge ndigits in Ruby 3.4 (ok in 3.2) https://bugs.ruby-lang.org/issues/21650 * Author: koilanetroc (Oleg Tolmashov) * Status: Open * ruby -v: ruby 3.4.7 (2025-10-08 revision 7a5688e2a2) +PRISM [arm64-darwin24] * Backport: 3.2: UNKNOWN, 3.3: UNKNOWN, 3.4: UNKNOWN ---------------------------------------- ## 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 ```ruby 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: ```ruby 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 (ideally O(1)). -- https://bugs.ruby-lang.org/
Issue #21650 has been updated by mame (Yusuke Endoh). 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: ```ruby # 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? ---------------------------------------- Bug #21650: Performance regression: Rational#floor(ndigits) extremely slow for huge ndigits in Ruby 3.4 (ok in 3.2) https://bugs.ruby-lang.org/issues/21650#change-114940 * Author: koilanetroc (Oleg Tolmashov) * Status: Open * ruby -v: ruby 3.4.7 (2025-10-08 revision 7a5688e2a2) +PRISM [arm64-darwin24] * Backport: 3.2: UNKNOWN, 3.3: UNKNOWN, 3.4: UNKNOWN ---------------------------------------- ## 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 ```ruby 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: ```ruby 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 ышьшдфкдн фы -- https://bugs.ruby-lang.org/
Issue #21650 has been updated by koilanetroc (Oleg Tolmashov). 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:
```ruby # 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 ---------------------------------------- Bug #21650: Performance regression: Rational#floor(ndigits) extremely slow for huge ndigits in Ruby 3.4 (ok in 3.2) https://bugs.ruby-lang.org/issues/21650#change-114941 * Author: koilanetroc (Oleg Tolmashov) * Status: Open * ruby -v: ruby 3.4.7 (2025-10-08 revision 7a5688e2a2) +PRISM [arm64-darwin24] * Backport: 3.2: UNKNOWN, 3.3: UNKNOWN, 3.4: UNKNOWN ---------------------------------------- ## 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 ```ruby 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: ```ruby 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 ышьшдфкдн фы -- https://bugs.ruby-lang.org/
Issue #21650 has been updated by mame (Yusuke Endoh). 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: ```ruby 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. ---------------------------------------- Feature #21650: Performance regression: Rational#floor(ndigits) extremely slow for huge ndigits in Ruby 3.4 (ok in 3.2) https://bugs.ruby-lang.org/issues/21650#change-114943 * Author: koilanetroc (Oleg Tolmashov) * Status: Feedback ---------------------------------------- ## 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 ```ruby 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: ```ruby 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 ышьшдфкдн фы -- https://bugs.ruby-lang.org/
Issue #21650 has been updated by koilanetroc (Oleg Tolmashov). Sure, thanks for clarification and fast reply. ---------------------------------------- Feature #21650: Performance regression: Rational#floor(ndigits) extremely slow for huge ndigits in Ruby 3.4 (ok in 3.2) https://bugs.ruby-lang.org/issues/21650#change-114944 * Author: koilanetroc (Oleg Tolmashov) * Status: Feedback ---------------------------------------- ## 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 ```ruby 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: ```ruby 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 ышьшдфкдн фы -- https://bugs.ruby-lang.org/
participants (2)
-
koilanetroc (Oleg Tolmashov) -
mame (Yusuke Endoh)