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
drop millions of allocations by using a linked list #1188
Conversation
|
Also the runtime test: master: This branch: |
|
@tenderlove: Awesome.. |
|
This is one of the coolest merge requests I've seen for so many reasons. |
|
Yeah! Really awesome! |
|
I'd believe it. But look at the linked list that resolver uses for exactly the same reasons, maybe we can use it here too. |
|
@evanphx where can I find it? I don't know the resolver code very well. |
|
@tenderlove I believe the code you are looking for is here: |
|
@tomciopp thanks! I've updated the PR to use the built-in linked list class. I think we can implement the current instance method |
| stack = Gem::List.new(dep_spec, trail) | ||
| block[dep_spec, stack] | ||
| spec_name = dep_spec.name | ||
| traverse(dep_spec, stack, &block) unless |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I may be wrong but shouldn't this be _traverse instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding a?
private_class_method :_traverse|
|
1 similar comment
|
|
| @@ -2,6 +2,8 @@ module Gem | |||
| List = Struct.new(:value, :tail) | |||
|
|
|||
| class List | |||
| include Enumerable | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you include Enumerable you don't need the explicit find implementation anymore. The to_a implementation could go too if Struct didn't have a default one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather do that in a different commit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
|
|
Is the merge button you have to click @tenderlove, |
|
@libo hah, I closed and reopened to get travis to build again. |
|
:-) eheh By this PR was a hell of a #fridayhug thanks @tenderlove ! |
|
Mind blown. Great job. |
Use a linked list to drop array allocations. The previous
implementation would dup arrays on every call to `traverse`. This patch
uses a linked list so that any block that is interested in keeping a
reference to `trail` can just keep the trail node around (the trail node
points at it's parents).
This approach reduces allocations from 11173668, to 2940.
Here is the test I used:
```ruby
require 'stackprof'
require 'allocation_tracer'
require 'rubygems/test_case'
require 'rubygems/ext'
require 'rubygems/specification'
require 'benchmark'
class TestGemSpecification < Gem::TestCase
def test_runtime
make_gems do
StackProf.run(mode: :wall, out: '/tmp/out.dump') do
assert_raises(LoadError) { require 'no_such_file_foo' }
end
end
end
def test_alone
make_gems do
tms = Benchmark.measure {
assert_raises(LoadError) { require 'no_such_file_foo' }
}
p tms.total
assert_operator tms.total, :<=, 10
end
end
def test_memory
make_gems do
ObjectSpace::AllocationTracer.setup(%i{path line type})
r = ObjectSpace::AllocationTracer.trace do
assert_raises(LoadError) { require 'no_such_file_foo' }
end
r.sort_by { |k,v| v.first }.each do |k,v|
p k => v
end
p hash_alloc: ObjectSpace::AllocationTracer.allocated_count_table[:T_HASH]
p array_alloc: ObjectSpace::AllocationTracer.allocated_count_table[:T_ARRAY]
p :TOTAL => ObjectSpace::AllocationTracer.allocated_count_table.values.inject(:+)
end
end
def make_gems
save_loaded_features do
num_of_pkg = 7
num_of_version_per_pkg = 3
packages = (0..num_of_pkg).map do |pkgi|
(0..num_of_version_per_pkg).map do |pkg_version|
deps = Hash[(pkgi..num_of_pkg).map { |deppkgi| ["pkg#{deppkgi}", ">= 0"] }]
new_spec "pkg#{pkgi}", pkg_version.to_s, deps
end
end
base = new_spec "pkg_base", "1", {"pkg0" => ">= 0"}
Gem::Specification.reset
install_specs base,*packages.flatten
base.activate
yield
end
end
end
```
Before:
{:TOTAL=>11173668}
After:
{:TOTAL=>2940}
|
Wow awesome. Nice find! |
|
Way to go @tenderlove |
|
8 O |
|
Awesome work @tenderlove, can't wait for this to be released |
|
Dude that's awesome! Good job. |
|
@tenderlove No matter what people on the internet say, what you're doing is fantastic. Please don't get discouraged. Thank you for spotting and optimising this! #FridayHug |
| stack = Gem::List.new(dep_spec, trail) | ||
| block[dep_spec, stack] | ||
| spec_name = dep_spec.name | ||
| _traverse(dep_spec, stack, &block) unless |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I may have missed something, but I think this should be traverse rather than _traverse as the former is the only one (I can see) with a 3 element signature.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you have that backwards; traverse takes 1 param + block, _traverse takes 2 + block. And in any case the Gem::List is generated prior to the invocation, so traverse wouldn't be appropriate anyhow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah sorry, scratch that. I was looking at an earlier version and the call is correct now.
|
|
drop millions of allocations by using a linked list
|
|
|
@mfazekas you are totally right. Think we should revert? |
|
@tenderlove i think it have to be reverted, and we need to add some test that fails with that implementation. |
|
@mfazekas do you have any idea how to make a test that fails with this implementation? I don't want to revert until we can prove it's wrong (no doubt it is wrong, just need to make a test). |
|
i'll try to came up with a testcase sometime later today |
|
#1191 shows is a testcase, demonstrating the issue with your optimization. But it also shows that the original one despite all the multiple reverse's and exponential algorithm will prefer earlier and not later versions. So we use find_in_unresolved_tree in case we cannot find a required file in any of the activated gems. The idea is that we check indirect dependencies of the activated gems and try to select a gem that contains the file, but doesn't cause a conflict. And from all of the possibilities we should prefer latest gem versions. Current implementation is has many issues as its exponential, might select conflicting gems (#1169) and might not use the latest versions of the possibilities (#1191). |
|
Some |
|
A valid reason to learn linked lists |
|
Hi @tenderlove, very cool. After seeing this commit on Ruby Weekly News, I took a look at this code for the first time. I just submitted a PR to hopefully improve the linked list implementation used here (#1200). Specifically, in the |
|
|

I'm sending a PR because the reduction in allocations is so surprising to me that I'm afraid it's wrong. All tests pass on my machine, and I think it's backwards compatible, but I need review. /cc @evanphx @drbrain
Use a linked list to drop array allocations. The previous
implementation would dup arrays on every call to
traverse. This patchuses a linked list so that any block that is interested in keeping a
reference to
trailcan just keep the trail node around (the trail nodepoints at it's parents).
This approach reduces allocations from 11173668, to 2940.
Here is the test I used:
Before:
{:TOTAL=>11173668}
After:
{:TOTAL=>2940}