Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lint/Loop is recommending bad performance #362

Open
bquorning opened this issue Jul 4, 2023 · 5 comments
Open

Lint/Loop is recommending bad performance #362

bquorning opened this issue Jul 4, 2023 · 5 comments

Comments

@bquorning
Copy link
Contributor

bquorning commented Jul 4, 2023

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.

@bquorning bquorning changed the title Lint/Loop is recommending bad performance Lint/Loop is recommending bad performance Jul 4, 2023
@HeyNonster
Copy link

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

@bquorning
Copy link
Contributor Author

bquorning commented Jul 4, 2023

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

@nirvdrum
Copy link

nirvdrum commented Jul 4, 2023

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.

@HeyNonster
Copy link

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

@bquorning
Copy link
Contributor Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants