Skip to content

Improve performance of Bundler::SpecSet#for by using hash lookup of handled deps#5537

Merged
deivid-rodriguez merged 1 commit intoruby:masterfrom
technicalpickles:specset-for-hash-lookup-of-handled-deps
Jun 19, 2022
Merged

Improve performance of Bundler::SpecSet#for by using hash lookup of handled deps#5537
deivid-rodriguez merged 1 commit intoruby:masterfrom
technicalpickles:specset-for-hash-lookup-of-handled-deps

Conversation

@technicalpickles
Copy link
Copy Markdown
Contributor

@technicalpickles technicalpickles commented May 13, 2022

What was the end-user or developer problem that led to this PR?

Looking for improvements to runtime speed of require 'bundler/setup'

What is your fix for the problem, implemented in this PR?

I was looking at (yet another) flamegraph in speedscope, and used the
'left hand heavy' and was shocked to realize that 0.5s of the 1.7s
is spent in DepProxy#name. This method only delegates the name to an
underlying spec, so it's not complex at all.

It seems to be of how often this line ends up calling it:

 next if handled.any?{|d| d.name == dep.name && (match_current_platform || d.__platform == dep.__platform) } || dep.name == "bundler"

The handled array is built up as dependencies are handled, so this get
slower as more dependencies are installed.

This change changes how handled is track. Instead of just an array, I've
tried using a Hash, with the key being a dep's name, and the value being
a list of deps with that name. This means it's constant time to find
the dependencies with the same name.

I saw a drop from 1.7s to 1.0s against master, and from 0.95s to 0.24s
when used with #5533

Make sure the following tasks are checked

@deivid-rodriguez
Copy link
Copy Markdown
Contributor

Oh my god, how did I not realize about this before? 😱 THANK YOU!

If I understand correctly, this improves things more than #5536, and would make #5536 no longer have an effect in performance, similarly to how #5533 made #5532 and #5534 unnecessary?

@deivid-rodriguez deivid-rodriguez force-pushed the specset-for-hash-lookup-of-handled-deps branch from 7d3f4a8 to 7578bcd Compare May 16, 2022 10:42
@deivid-rodriguez
Copy link
Copy Markdown
Contributor

This looks great to me, I run it against the rails/rails repository Gemfile and got the following:

➜  rails git:(main) ✗ hyperfine 'BUNDLER_VERSION=2.4.0.hashlookup ruby -rbundler/setup -e1' 'BUNDLER_VERSION=2.4.0.dev ruby -rbundler/setup -e1'
Benchmark 1: BUNDLER_VERSION=2.4.0.hashlookup ruby -rbundler/setup -e1
  Time (mean ± σ):     195.0 ms ±   0.9 ms    [User: 140.2 ms, System: 45.9 ms]
  Range (min … max):   193.1 ms … 196.6 ms    15 runs
 
Benchmark 2: BUNDLER_VERSION=2.4.0.dev ruby -rbundler/setup -e1
  Time (mean ± σ):     203.3 ms ±   0.7 ms    [User: 148.6 ms, System: 45.9 ms]
  Range (min … max):   201.9 ms … 204.2 ms    14 runs
 
Summary
  'BUNDLER_VERSION=2.4.0.hashlookup ruby -rbundler/setup -e1' ran
    1.04 ± 0.01 times faster than 'BUNDLER_VERSION=2.4.0.dev ruby -rbundler/setup -e1'

So an extra 4% speed up over current master branch!

@technicalpickles
Copy link
Copy Markdown
Contributor Author

If I understand correctly, this improves things more than #5536, and would make #5536 no longer have an effect in performance, similarly to how #5533 made #5532 and #5534 unnecessary?

I had a chance to test this against https://github.com/technicalpickles/big-gemfile

# master
❯ hyperfine "ruby run.rb 2.4.0.dev" --warmup 2                                                                                                      ─╯
Benchmark 1: ruby run.rb 2.4.0.dev
  Time (mean ± σ):     515.6 ms ±  23.8 ms    [User: 304.6 ms, System: 151.9 ms]
  Range (min … max):   482.9 ms … 556.4 ms    10 runs

# this branch
❯ hyperfine "ruby run.rb 2.4.0.dev" --warmup 2                                                                                                      ─╯
Benchmark 1: ruby run.rb 2.4.0.dev
  Time (mean ± σ):     480.8 ms ±  16.5 ms    [User: 278.7 ms, System: 151.4 ms]
  Range (min … max):   457.9 ms … 509.2 ms    10 runs

# this branch + https://github.com/rubygems/rubygems/pull/5536
❯ hyperfine "ruby run.rb 2.4.0.dev" --warmup 2                                                                                                      ─╯
Benchmark 1: ruby run.rb 2.4.0.dev
  Time (mean ± σ):     454.9 ms ±  17.7 ms    [User: 266.7 ms, System: 137.3 ms]
  Range (min … max):   434.4 ms … 480.8 ms    10 runs

I cracked open a flamegraph using both the branches, and I'm seeing LazySpecification#hash crop up again 😅 cc #5534 ... if we try all 3 branches together...

❯ hyperfine "ruby run.rb 2.4.0.dev" --warmup 2                                                                                                      ─╯
Benchmark 1: ruby run.rb 2.4.0.dev
  Time (mean ± σ):     425.1 ms ±  11.6 ms    [User: 233.4 ms, System: 136.2 ms]
  Range (min … max):   412.2 ms … 450.7 ms    10 runs

@deivid-rodriguez
Copy link
Copy Markdown
Contributor

I'm not seeing the same results with your sample repository 😮. I get the results that I would expect, namely, this PR improves performance by a lot, but once applied, #5534 and #5536 no longer have any effect.

➜  big-gemfile git:(main) ✗ hyperfine 'BUNDLER_VERSION=2.4.0.hash ruby -rbundler/setup -e1' 'BUNDLER_VERSION=2.4.0.hashmemo ruby -rbundler/setup -e1' 'BUNDLER_VERSION=2.4.0.hashtwomemos ruby -rbundler/setup -e1' 'BUNDLER_VERSION=2.4.0.dev ruby -rbundler/setup -e1'
Benchmark 1: BUNDLER_VERSION=2.4.0.hash ruby -rbundler/setup -e1
  Time (mean ± σ):     191.4 ms ±   1.0 ms    [User: 148.0 ms, System: 36.4 ms]
  Range (min … max):   189.8 ms … 193.3 ms    15 runs
 
Benchmark 2: BUNDLER_VERSION=2.4.0.hashmemo ruby -rbundler/setup -e1
  Time (mean ± σ):     190.8 ms ±   1.0 ms    [User: 147.6 ms, System: 36.3 ms]
  Range (min … max):   189.2 ms … 192.4 ms    15 runs
 
Benchmark 3: BUNDLER_VERSION=2.4.0.hashtwomemos ruby -rbundler/setup -e1
  Time (mean ± σ):     190.7 ms ±   0.8 ms    [User: 147.2 ms, System: 36.3 ms]
  Range (min … max):   189.4 ms … 192.3 ms    15 runs
 
Benchmark 4: BUNDLER_VERSION=2.4.0.dev ruby -rbundler/setup -e1
  Time (mean ± σ):     227.1 ms ±   2.6 ms    [User: 183.7 ms, System: 36.3 ms]
  Range (min … max):   225.1 ms … 234.7 ms    12 runs
 
Summary
  'BUNDLER_VERSION=2.4.0.hashtwomemos ruby -rbundler/setup -e1' ran
    1.00 ± 0.01 times faster than 'BUNDLER_VERSION=2.4.0.hashmemo ruby -rbundler/setup -e1'
    1.00 ± 0.01 times faster than 'BUNDLER_VERSION=2.4.0.hash ruby -rbundler/setup -e1'
    1.19 ± 0.01 times faster than 'BUNDLER_VERSION=2.4.0.dev ruby -rbundler/setup -e1'

@deivid-rodriguez deivid-rodriguez changed the title Improve performance of Bundler::SpecSet#for by using hash lookup of handled deps Improve performance of Bundler::SpecSet#for by using hash lookup of handled deps Jun 18, 2022
…andled deps

I was looking at (yet another) flamegraph in speedscope, and used the
'left hand heavy' and was shocked to realize that 0.5s of the 1.7s
is spent in DepProxy#name. This method _only_ delegates the name to an
underlying spec, so it's not complex at all.

It seems to be of how often this line ends up calling it:

     next if handled.any?{|d| d.name == dep.name && (match_current_platform || d.__platform == dep.__platform) } || dep.name == "bundler"

The `handled` array is built up as dependencies are handled, so this get
slower as more dependencies are installed.

This change changes how `handled` is track. Instead of just an array, I've
tried using a Hash, with the key being a dep's name, and the value being
a list of deps with that name. This means it's constant time to find
the dependencies with the same name.

I saw a drop from 1.7s to 1.0s against master, and from 0.95s to 0.24s
when used with ruby#5533
@deivid-rodriguez deivid-rodriguez force-pushed the specset-for-hash-lookup-of-handled-deps branch from 7578bcd to 844dac3 Compare June 18, 2022 19:48
@deivid-rodriguez
Copy link
Copy Markdown
Contributor

Let's do this!

@deivid-rodriguez deivid-rodriguez merged commit 4b1b2b7 into ruby:master Jun 19, 2022
deivid-rodriguez added a commit that referenced this pull request Jun 29, 2022
…p-of-handled-deps

Improve performance of `Bundler::SpecSet#for` by using hash lookup of handled deps

(cherry picked from commit 4b1b2b7)
@technicalpickles technicalpickles deleted the specset-for-hash-lookup-of-handled-deps branch July 11, 2022 18:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants