-
Notifications
You must be signed in to change notification settings - Fork 125
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
Optimize URLHelpers.gather_constants
#1935
Conversation
url_helpers_module = Rails.application.routes.named_routes.url_helpers_module | ||
path_helpers_module = Rails.application.routes.named_routes.path_helpers_module |
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.
Extracting out these local variables removes the need to do repeated constant look-ups in the hot path below.
next false unless mod < url_helpers_module || | ||
mod < path_helpers_module || | ||
mod.singleton_class < url_helpers_module || | ||
mod.singleton_class < path_helpers_module |
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.
Most classes/modules don't include these modules, so it's worth adding a fast path, even if it slows the cases where the modules are included.
This roughly doubled the speed of this span, compared to not having it.
end | ||
|
||
ancestors = Set.new.compare_by_identity.merge(ancestors_of(mod)).subtract(superclass_ancestors) |
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.
The allocations and set operations here where the majority of the hot path.
mod.singleton_class < url_helpers_module || | ||
mod.singleton_class < path_helpers_module | ||
|
||
includes_helper?(mod, url_helpers_module) || |
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.
Observation: this computes and traverses the ancestor chain for mod
and mod.singleton_class
twice each.
I tried doing it just once, like so:
mod_direct_ancestors = direct_ancestors_of(mod)
next true if mod_direct_ancestors.include?(url_helpers_module) ||
mod_direct_ancestors.include?(path_helpers_module)
mod_meta_direct_ancestors = direct_ancestors_of(mod.singleton_class)
mod_meta_direct_ancestors.include?(url_helpers_module) ||
mod_meta_direct_ancestors.include?(path_helpers_module)
using:
sig { params(mod: Module).returns(T::Array[Module]) }
private def direct_ancestors_of(mod)
if Class === mod
superclass = superclass_of(mod)
ancestors_of(mod).take_while { |a| !superclass.equal?(a) }
else
ancestors_of(mod)
end
end
But it turned out to be roughly 25% slower
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.
We have a bunch of methods returning arrays in https://github.com/Shopify/tapioca/blob/35a6a43b5469d5b03d292330449fd7b6db7593a6/lib/tapioca/runtime/reflection.rb that could benefit from returning sets instead.
2344fdf
to
95f3419
Compare
95f3419
to
70d7fb5
Compare
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.
Thanks for running these benchmarks and invalidating the Set
idea! ❤️
`mod < url_helpers_module` gave false positives for weird classesl like `XPath`. They could survive through that first layer of filtering, and then require special handling later, by forcing us to use `.any? { |a| helper == a}` instead of the simpler (and faster) `.include?(helper)` check. By flipping this around, the receiver is always one of these known module types, which will always be hitting the usual `Module#>` implementation, which will filter out `XPath` early on. That frees us up to switch back to the faster `.include?(helper)` variant.
I found one more trick which both simplifies the code ( See the commit message of ef5e6c9 for details. |
Motivation
Profiling
tapioca DSL SomeConst
identified this method as a hot spot, taking roughly 1 out of the 6 total second spent by Tapioca (not including application load time).The changes in this PR speed up running
tapioca dsl
against Shopify core by 1 entire second!Benchmark results (writeup WIP)
Benchmark run details
Master
My original approach
Doesn't handle
prepend
ed modules correctly.2344fdf
Ufuk's improvement
Fixes logic for
prepend
ed modules.70d7fb5
Final improvement
See the commit message of ef5e6c9
Using a
Set
(poorly)Uses a
Set
to accelerate the.include?
checks. However, since thisSet
isn't reused, the overhead of created it is larger than the benefit of the faster lookup.https://github.com/Shopify/tapioca/compare/70d7fb5491fe655715da1e7f52dbd8ac1085418e..684c6d7a98a612f268f814d8f079f0c6756a216b
Using a set (properly)
Ammortize the
Set
construction over two consecutive.include?
checkshttps://github.com/Shopify/tapioca/compare/70d7fb5491fe655715da1e7f52dbd8ac1085418e..c31a9ef73c59a8965ec0679440b10ee7c3e90379
other toy micro-benchmarks