rubocop-performance
                                
                                
                                
                                    rubocop-performance copied to clipboard
                            
                            
                            
                        `Lint/Loop` is recommending bad performance
Is your feature request related to a problem? Please describe.
Lint/Loop recommends what we use a loop block instead of while or until. But looking at the performance of each, I think the recommendation should be the other way around.
After reading https://mastodon.social/@noteflakes/110652154677203990 I used the gist https://gist.github.com/noteflakes/d48bb74737577d1e7e6ab3954270325a to measure that loop is between 1.5 and 2.0 times slower than while or until (on my machine). The Mastodon post mentions that with yjit, the performance difference is even larger (up to 9x).
Describe the solution you'd like
Perhaps the Lint/Loop cop should be retired, and a Performance/Loop cop should be introduced instead, recommending that people don’t use loop when while or until can be used instead.
On my machine without YJIT:
Ruby: 3.2.2
YJIT Enabled: false
Warming up --------------------------------------
                loop   256.000  i/100ms
               while   489.000  i/100ms
Calculating -------------------------------------
                loop      2.597k (± 0.8%) i/s -     13.056k in   5.027015s
               while      5.023k (± 1.0%) i/s -     25.428k in   5.063176s
Comparison:
               while:     5022.6 i/s
                loop:     2597.3 i/s - 1.93x  (± 0.00) slower
With YJIT enabled:
Ruby: 3.2.2
YJIT Enabled: true
Warming up --------------------------------------
                loop   447.000  i/100ms
               while     2.129k i/100ms
Calculating -------------------------------------
                loop      4.482k (± 1.0%) i/s -     22.797k in   5.086960s
               while     21.338k (± 0.9%) i/s -    108.579k in   5.088921s
Comparison:
               while:    21338.2 i/s
                loop:     4481.9 i/s - 4.76x  (± 0.00) slower
                                    
                                    
                                    
                                
The performance difference exists all the way back to Ruby 2.7 (the oldest version supported by rubocop-performance):
ruby 2.7.8p225 (2023-03-30 revision 1f4d455848) [arm64-darwin22]
Warming up --------------------------------------
                loop   284.000  i/100ms
               while   574.000  i/100ms
Calculating -------------------------------------
                loop      2.821k (± 1.7%) i/s -     14.200k
               while      4.648k (± 4.4%) i/s -     23.534k
Comparison:
               while:     4647.9 i/s
                loop:     2821.3 i/s - 1.65x slower
                                    
                                    
                                    
                                
Can you please post results with 3.3.0-dev? I'm seeing the performance gap is much smaller than it is in 3.2.2. I'm curious if you see the same.
I don't think this should be a recommendation now that Ruby has several JIT compilers. This is the sort of thing a JIT can and should optimize. JIT compilers work best with idiomatic code; it's really hard for them to optimize code written in a clever way to exploit implementation details of the particular Ruby interpreter. E.g., on TruffleRuby there is no performance difference between the two.
If a while loop is the natural way to solve your problem, go for it. But, manually inlining code to avoid block overhead is an optimization I don't think is going to age terribly well.
Certainly much narrower in Ruby 3.3:
ruby 3.3.0dev (2023-07-04T14:45:29Z master 296782ab60) [arm64-darwin22]
Warming up --------------------------------------
                loop   326.000  i/100ms
               while   496.000  i/100ms
Calculating -------------------------------------
                loop      3.318k (± 1.4%) i/s -     16.626k in   5.011265s
               while      5.011k (± 0.9%) i/s -     25.296k in   5.048242s
Comparison:
               while:     5011.3 i/s
                loop:     3318.4 i/s - 1.51x  slower
YJIT:
ruby 3.3.0dev (2023-07-04T14:45:29Z master 296782ab60) +YJIT [arm64-darwin22]
Warming up --------------------------------------
                loop     2.368k i/100ms
               while     3.810k i/100ms
Calculating -------------------------------------
                loop     23.762k (± 1.2%) i/s -    120.768k in   5.083200s
               while     37.003k (± 1.1%) i/s -    186.690k in   5.045846s
Comparison:
               while:    37002.8 i/s
                loop:    23761.5 i/s - 1.56x  slower
                                    
                                    
                                    
                                
Here’s the benchmark running on Ruby 3.3.0 without yjit, showing that while is much faster than loop:
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin22]
Warming up --------------------------------------
                loop   285.000 i/100ms
               while     1.100k i/100ms
Calculating -------------------------------------
                loop      2.826k (± 1.0%) i/s -     14.250k in   5.042247s
               while     10.987k (± 0.7%) i/s -     55.000k in   5.006336s
Comparison:
               while:    10986.7 i/s
                loop:     2826.4 i/s - 3.89x  slower