Skip to content

Ruby: restrict join order of API graph predicates#12804

Merged
asgerf merged 3 commits intogithub:mainfrom
asgerf:rb/api-graphs-cached
Apr 17, 2023
Merged

Ruby: restrict join order of API graph predicates#12804
asgerf merged 3 commits intogithub:mainfrom
asgerf:rb/api-graphs-cached

Conversation

@asgerf
Copy link
Copy Markdown
Contributor

@asgerf asgerf commented Apr 12, 2023

Places join-order restrictions on API graphs to more reliably get a good join order at the use-sites. This is done by caching more predicates, and placing inline facade predicates in front to enforce a good join ordering at the call site.

While working on adding better support for instance members in API graphs, I noticed the join order in some library models would go haywire seemingly at random, and this PR is meant to stop that from happening.

To explain, a chain of API-graph calls should be evaluated from left to right. For example,

API::getTopLevelMember("Bar").getAMethodCall("baz")

Should be evaluated by first finding uses of Bar and then calls to baz on those objects.

But sometimes it gets evaluated backwards, by starting at calls to baz and then checking if their receiver is Bar (it's more complex than that, but that's the basic idea). In this small example it's not so bad, but in practice backwards evaluation is really bad. Previously this was left mostly to the optimizer to figure out, which it very often did, but not always.

The PR also removes some uses of getASubclass() which were redundant because it came before a member predicate that itself performs getASubclass(). The optimizer did not handle that very well.

Evaluation shows an average speed-up of about 2%.

@asgerf asgerf added no-change-note-required This PR does not need a change note Ruby labels Apr 12, 2023
@asgerf asgerf marked this pull request as ready for review April 13, 2023 08:15
@asgerf asgerf requested a review from a team as a code owner April 13, 2023 08:15
@aschackmull
Copy link
Copy Markdown
Contributor

You might get better predictability of joins by using pragma[late_inline] combined with a bindingset. That kind of join order hint is a bit more high-level and easier to reason about.

@asgerf
Copy link
Copy Markdown
Contributor Author

asgerf commented Apr 13, 2023

@aschackmull I used pragma[inline_late] for getAValueReachableFromSource where it is a significant improvement over inlining the only_bind pragmas. In the other cases, I refrained from using it for a few reasons:

  • For getMethod(string m), it differs from call site to call site whether the parameter m is an input or output. Since we can only have one binding set, we'd need two variants of the predicate to support all existing use cases.
  • Readability. It can't be applied directly to member predicates, which makes it a bit of a hassle to work with.
  • For some predicates inline_late seemed to produce a slightly larger tuple count than inlining only_bind pragmas, so given the readability issue it just didn't seem like a win.

@aschackmull
Copy link
Copy Markdown
Contributor

@kaspersv FYI ^

@asgerf asgerf force-pushed the rb/api-graphs-cached branch from 4bdd756 to 69cb138 Compare April 14, 2023 09:02
Copy link
Copy Markdown
Contributor

@hvitved hvitved left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just a couple of questions.

// Class methods
API::getTopLevelMember("ActiveStorage")
.getMember("Blob")
.getASubclass()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this removed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the PR description:

The PR also removes some uses of getASubclass() which were redundant because it came before a member predicate that itself performs getASubclass(). The optimizer did not handle that very well.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I missed that.

ServiceInstantiation() {
this =
API::getTopLevelMember("Twirp").getMember("Service").getASubclass().getAnInstantiation()
this = API::getTopLevelMember("Twirp").getMember("Service").getAnInstantiation()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here getASubclass has been removed, because asking for getAnInstantiation will include instantiations of sub classes?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, again, from the PR description:

The PR also removes some uses of getASubclass() which were redundant because it came before a member predicate that itself performs getASubclass(). The optimizer did not handle that very well.

* Same as `getMember` but without join-order hints.
*/
cached
Node getMemberInternal(string m) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to have such internal predicates be either top-level predicates or on a an internal Impl class.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to an internal class at API::Node::Internal.

@asgerf asgerf merged commit ccb57f2 into github:main Apr 17, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-change-note-required This PR does not need a change note Ruby

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants